在 Unity 裡製作演出時,幾乎必然會需要跳過(Skip)功能。
結果畫面、轉蛋演出、故事段落。想做的只是「按下按鈕就把正在執行的演出跳到下一步或最終狀態」,但每次實作都很痛苦。
我本人對這個問題困擾很久。最後得到的做法是名為 ChainRacePattern (暫名) 的設計。
※ 本文介紹的 ChainRacePattern,現階段為提案階段的設計模式。
※ 更詳細的設計與實作說明已整理在後續文章。
Unityの演出スキップ問題をどう解決するか ー ChainRacePattern
沒有跳過的演出是很單純的。
移動A → 移動B → 移動C → 完成
但一旦加入「按下跳過按鈕就立刻到最終狀態」的需求,情況就大不同了。
常見的做法是,在跳過時個別停止正在執行的動畫(tween)。
void OnSkipButtonPressed()
{
if (tweenA != null && tweenA.IsPlaying()) tweenA.Kill();
if (tweenB != null && tweenB.IsPlaying()) tweenB.Kill();
if (tweenC != null && tweenC.IsPlaying()) tweenC.Kill();
rect.anchoredPosition = finalPosition;
isSkipped = true;
}
隨著演出越多,就會越難管理「現在哪個在動」以及「跳過時要把狀態推到哪裡」。
每次新增演出都要修改跳過處理,造成 演出邏輯與跳過邏輯高度耦合,這就是痛點的本質。
因此,我把演出的各個要素統一成名為 Chain 的物件。
Chain 的概念很單純:「可以開始(start)、會在某時完成(complete)、必要時可以被跳過(skip)」。
await new ChainSequence(
new ChainDelay(0.5f),
new ChainAction(() => Debug.Log("Hello")),
new ChainDelay(1.0f)
).Start();
await new ChainParallel(
ChainMoveTween(rectA, targetA, 1.0f),
ChainMoveTween(rectB, targetB, 1.0f)
).Start();
到這裡為止看起來都蠻自然的。問題在於,如何表現「跳過」這個行為。
如果把按鈕輸入也視為「開始後按下就完成」的行為,那它也可以是一個 Chain。
new ChainButton(skipButton)
有了這個想法,接下來就是組合的問題。
ChainRace 會同時執行多個 Chain,並在「任一個完成的時候」立刻把其餘的跳過並結束。
await new ChainRace(
new ChainButton(skipButton),
new ChainSequence(
ChainMoveTween(rect, pos1, 1.0f),
ChainMoveTween(rect, pos2, 1.0f)
)
).Start();
這個設計的意義很單純:
也就是說,不把跳過當作例外處理,而是把它當成「輸入與演出之間的競賽(race)」來表現。
把「輸入也當成 Chain,然後用 Race 把它們競賽」的想法,我稱為 ChainRacePattern。
自訂 Chain 時有三個基本要點:
StartInternal()Complete()SkipInternal()isFastForwardtrue。用來避開不必要的處理每個 Chain 都擁有自己的跳過處理,這樣可以避免在外層增加大量跳過分支判斷。
Scene1 是一個矩形依序從位置1→2→3→4 移動的動畫,用以示範 ChainRace 的不同用法。
new ChainRace(
new ChainButton(skipButton),
new ChainSequence(移動1→2, 移動2→3, 移動3→4)
)
按下按鈕會將整個演出一次性跳過。
new ChainRace(new ChainButton(skipButton), 移動1→2),
new ChainRace(new ChainButton(skipButton), 移動2→3),
new ChainRace(new ChainButton(skipButton), 移動3→4)
按下按鈕時,只跳過當下的區段,然後進入下一段。
new ChainRace(new ChainButton(skipButton), 移動1→2),
new ChainSequence(移動2→3),
new ChainRace(new ChainButton(skipButton), 移動3→4)
只有區段2會被強制完整播放、不允許跳過。
換句話說,只要控制是否用 ChainRace 包起來,就能方便地切換該區段是否允許跳過。
Scene2 是一個針對結果畫面設計的實作範例。
new ChainSequence(
new ChainRace(
new ChainButton(screenButton),
new ChainParallel(
fadePanel.ChainFade(false),
resultDialog.ChainShowDialog()
)
),
new ChainRace(
new ChainButton(screenButton),
resultDialog.ChainShowBonus()
),
ChainTouchScreen(),
new ChainParallel(
resultDialog.ChainHideDialog(),
fadePanel.ChainFade(true)
)
)
從上到下只需閱讀流程就能理解:
只用 Sequence、Parallel、Race 就能很直觀地描述演出流程與跳過控制。
事後才知道,這個想法類似於 JavaScript 的 Promise.race() 或 Rx 的 Amb。
但在遊戲演出場景中,單純「忽略輸的那一方」是不夠的。
被跳過的一方也必須「正確地落到最終狀態」,因此我們需要 SkipInternal() 與 isFastForward 這類機制來確保被跳過時也能完成收尾。
ChainRacePattern 的重點如下:
ChainChainChainRace 把輸入與演出進行競賽(race)ChainRace 包起來,就能切換該區段的可跳過性Chain 自行負責自己的跳過處理這樣新增演出時比較不會破壞跳過邏輯,跳過用的分支也不會散落在外層。
我用這個方式大幅降低了過去遇到的痛點。
注意: ChainRacePattern 目前仍屬於設計模式提案。
它不是一個已完備的函式庫,而是作為實作範例公開。為了保持最小構成,有需要的話請依用途或專案自行擴充與調整功能。
有興趣的話可以看看倉庫與示範,也歡迎任何回饋。
以 MIT License 發布,必要時可自由修改與使用。