はじめに
Unity で演出を作っていると、ほぼ確実にスキップ機能が必要になります。
リザルト画面、ガチャ演出、ストーリーパート。やりたいことは「ボタンが押されたら、実行中の演出を飛ばして次へ進む」だけなのに、実装は毎回つらい。
私自身、この問題にかなり悩みました。その末にたどり着いたのが ChainRacePattern (仮) です。
※ 本記事で紹介する ChainRacePattern は、現時点では提案段階の設計パターンです。
※ 詳しい設計・実装の話は続編にまとめました。
Unityの演出スキップ問題をどう解決するか ー ChainRacePattern
- リポジトリ: ChainRacePatternUnity
-
デモ(基本操作): Scene1
-
デモ(リザルト演出): Scene2
演出スキップの何がつらいのか
スキップがない演出は単純です。
移動A → 移動B → 移動C → 完了
しかし「スキップボタンを押したら即座に最終状態にする」が入った途端、話が変わります。
よくあるのは、スキップ時に実行中のアニメーションを個別に止める実装です。
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 というオブジェクトで統一しました。
Chain は「開始できて、いつか完了し、必要ならスキップもできる」という単純なものです。
ChainSequence:順次実行
await new ChainSequence(
new ChainDelay(0.5f),
new ChainAction(() => Debug.Log("Hello")),
new ChainDelay(1.0f)
).Start();
ChainParallel:並列実行
await new ChainParallel(
ChainMoveTween(rectA, targetA, 1.0f),
ChainMoveTween(rectB, targetB, 1.0f)
).Start();
ここまでは比較的自然です。
問題は、スキップをどう表現するかでした。
気づき:ユーザー入力も Chain にできる
ボタン入力も「開始して、押されたら完了する」と考えれば、これも Chain です。
new ChainButton(skipButton)
ここまで来ると、あとは組み合わせの問題になります。
ChainRace:最初に完了した方が勝ち
ChainRace は複数の Chain を同時に実行し、どれか1つが完了した時点で残りをスキップして終了します。
await new ChainRace(
new ChainButton(skipButton),
new ChainSequence(
ChainMoveTween(rect, pos1, 1.0f),
ChainMoveTween(rect, pos2, 1.0f)
)
).Start();
この意味はシンプルです。
- アニメーションが先に終わる → 入力待ちをスキップして通常再生
- ボタンが先に押される → アニメーションをスキップして次へ進む
つまり、スキップを例外処理ではなく、入力と演出の競争として表現できます。
この「入力も Chain として扱い、Race で競わせる」考え方を ChainRacePattern と呼んでいます。
Chain の基本ルール
独自の Chain を作る際の基本は次の3つです。
-
StartInternal()
開始処理を書く。完了時にComplete()を呼ぶ -
SkipInternal()
開始後のスキップ時に直ちに終了状態へ進める -
isFastForward
開始直後にSkip()確定ならtrue。不要な処理を省略するために利用する
各 Chain が自分のスキップ処理を持つことで、外側にスキップ用の分岐を増やさずに済みます。
実例1:Scene1 スキップの3パターン
Scene1 では、矩形が位置1→2→3→4と移動するアニメーションで、ChainRace の使い分けを示しています。
1. 全体スキップ
new ChainRace(
new ChainButton(skipButton),
new ChainSequence(移動1→2, 移動2→3, 移動3→4)
)
ボタンを押すと、演出全体が一気にスキップされます。
2. セクション単位スキップ
new ChainRace(new ChainButton(skipButton), 移動1→2),
new ChainRace(new ChainButton(skipButton), 移動2→3),
new ChainRace(new ChainButton(skipButton), 移動3→4)
押した時点のセクションだけをスキップし、次へ進みます。
3. スキップ不可区間
new ChainRace(new ChainButton(skipButton), 移動1→2),
new ChainSequence(移動2→3),
new ChainRace(new ChainButton(skipButton), 移動3→4)
セクション2だけ必ず最後まで再生されます。
つまり、ChainRace で囲むかどうかだけでスキップ可否を切り替えられます。
実例2:Scene2 リザルト演出
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 だけで、演出フローとスキップ制御をかなり素直に書けます。
Promise.race や Rx との関係
後から知ったのですが、この考え方は JavaScript の Promise.race() や Rx の Amb に近いです。
ただし、ゲーム演出では「負けた方を無視する」だけでは足りません。
スキップされた側も正しい最終状態に着地する必要があるので、SkipInternal() や isFastForward を用意しています。
まとめ
ChainRacePattern のポイントは次の通りです。
- 演出の各要素を
Chainで統一する - ユーザー入力も
Chainとして表現する ChainRaceで入力と演出を競わせるChainRaceで囲むかどうかでスキップ可否を制御できる- 各
Chainが自分のスキップ処理を持つ
演出を追加してもスキップ処理が壊れにくく、スキップ用の分岐が外側に散りにくい。
感じていたつらさを、この仕組みでかなり減らせました。
注意: ChainRacePattern は現時点では提案段階の設計パターンです。
完成品のライブラリではなく、あくまで実装例として公開しています。必要最低限の構成に留めているため、用途や案件に応じて機能追加・調整して使うことを想定しています。
興味があれば、リポジトリやデモを見てみてください。フィードバックをいただけると励みになります。
MIT License で公開しているので、必要に応じて改変・利用してください。
- リポジトリ: ChainRacePatternUnity
- デモ(基本操作): Scene1
- デモ(リザルト演出): Scene2
- 続編(詳しい設計や実装の話): Unityの演出スキップ問題をどう解決するか ー ChainRacePattern