見出し画像

演出の完了を待たずに次の演出を再生(Unityメモ)

タイトルはラジオ体操のように読んでください。

ユニットを矢継ぎ早に動かしたい

周回ゲームのようなものを作っています。何らかの戦闘要素は入れたい。でも周回時間は短くしたいです。
よくある対処としては再生を加速するような方法がありますが、それだと見ていてせわしないです。でも所要時間は減らしたい。
そんなわがままな欲求(要求ではない)を少し満たすため、あるユニットの行動の途中に次のユニットの行動を始めるような動作を組むことにしました。これもよくある演出ですが、矢継ぎ早に動くような感じも出るので悪くなさそう。
なお、戦闘要素は次のようなものを考えています。
・自動戦闘。戦闘中にユーザ入力なし
・あらかじめ戦闘ログを生成してから、ログを再生するという形式をとる
・戦闘はラウンド制。1ラウンド中に1人1回手番(ターン)が回ってくる

作った演出

行動開始から以前組んだシーン遷移まで、動画を撮ってみました。見どころは最初の行動の途中から次の行動が始まる部分です。左右のバーの実装と、行動名やエフェクト類の設定などは今後のタスクです。

センスのない行動名を心がけました

見どころを静止画でピックアップします。

下段中央の自陣ユニットから上段右の敵陣ユニットへエフェクトを飛ばす
上段右の敵陣ユニットに何かが命中した演出。このとき次の行動演出が始まっている
前の行動演出はまだ続いたまま、下段右の敵ユニットが少し動いて上段左の自陣ユニットに攻撃するような演出が再生される

以下、実装の方針を長々と書いています。

次に進めるタイミングを拾う(イベント駆動)

ある行動演出の途中に次の行動演出が始まる、ということは、行動の完了を待つことができないまま、次の行動演出を始めるタイミングを決める、ということになります。タイマーで決め打ちする方法もありますが、その場合でも演出の再生時間は行動のエフェクトごとにバラバラです。
そこで、演出データ側から「次に進める」キュー(cueの方)を出して次の行動の演出を再生することにしました。つまりイベント駆動型のアプローチです。

雑なシーケンス図でのイメージ。ターンは一人ぶんの行動を指す

幸いにも、UnityのTimelineにはSignalを通知する機能と、Signal受信時のハンドラを指定する機能があります。これらを使うことにしました。

Signal Trackを使ってSignal ReceiverにSignalを送信するやり方がGUI上でも見やすく、実装も分かりやすかったです。

同じSignalを受けるが、再生する演出を変える(状態遷移)

この記事のポイントです。テラシュールブログでもさらっと書かれているのですが、結構めんどくさかったです。

というのも、Signal Receiverでは一種類のSignalに対して複数のハンドラを使い分けることができません。
そのため、次に進むSignalを拾って複数の演出を再生しようとすると、次のどちらかの方法をとることになりますが、無策で臨むと上手くいきません。
・演出の種類の数だけSignalAssetとハンドラを用意する
・Signalとハンドラは共通の1種類を使い、ハンドラの中で条件分岐する
前者の方法はエフェクトを追加実装するたびにSignalAssetも追加しなければならないので、実質的に採用できません。しかもゲームの場合、次に再生する演出も戦闘の展開によってバラバラなので、(受信した演出ごとのSignal × 次に再生する演出) の組み合わせが膨大なことになります。
後者の条件分岐を使うにしても、演出ごとに一つ一つ分岐するのは前者と同等で無理。行動開始・命中といった行動のステップごとに分岐するにしても、一つのswitch文で対処するのはコードが読みづらそうです。しかも行動だけではなく戦闘開始時や終了時の勝利・敗北の演出なども分岐の対象になります。
そこで、シーンオブジェクトに対して戦闘のステップを一つの状態として定義しておき、ステップごとに状態遷移させることでSignal受信時に再生する演出を切り替えることにしました。この方法なら、演出の種類を増やす場合もコードを追加せずに済みます。こちらの場合、Signal Receiverのリスナはシーン管理オブジェクトとなります。

シーン管理オブジェクトを状態遷移させて再生する演出を切り替える。こちらを実装

ちなみにシーンに集約しない形でも実現はできると思いますが、Timelineインスタンスにリスナを毎回登録する処理が面倒そうだと思ったので採用しませんでした。

(参考)Timeline間でのイベント駆動による連続再生。Signal Receiverを介した数珠つなぎとなる

状態遷移とイベント駆動を合わせる

以前に実装した状態遷移の仕組みとUnityのイベントシステムを組み合わせていきます。

組み合わせていくのですが、Unityのイベント発生の順序や特性にゲームシステム側で行いたい処理の順序を合わせこんで、演出の再生タイミングを上手くつなげるところが難しかったです。だいたい状態遷移せずに演出が終わってしまいます。
まあ、色々動かしたい仕組みがあったからなんですが…プレハブが絡むことで一段階難しくなった感じ。

Unityのイベントハンドラの実行順とTimelineの初期設定:Startでは遅すぎる

言葉で書いても分かりづらかったので、最初に初期化処理のシーケンス図を載せます。

初期化処理のシーケンス図。黒:Awake、橙:OnEnable、白:Start、赤:Update
実際の処理では、演出オブジェクトは1行動分の演出ステップ×行動するユニット の数だけロードされる

UnityでのGameObjectのイベントハンドラの実行順について、ざっくり書くと以下の通り。

・ゲーム開始時にDisabled(activeSelfがfalse)なオブジェクトは初期化処理がなされない
・Awake→OnEnable→Start→フレームごとにUpdate、の順にイベントが発生
・Awake, Startは一度だけしか実行されない(特にStart)
・OnEnableはSetActiveで有効になるたびに実行される/できる
・コンポーネントのスクリプトの実行順はScripting Execution Orderに依存

特にコンポーネント間で依存性がある場合に実行順が重要になるのですが、これらはコンポーネントを初期設定するステップをイベントの発生順に合わせることでそれなりに制御できるようになりました。
あと自作のシステムの事情として、Unityシーンの遷移ではなくGUI表示の切り替えで見かけ上シーン遷移をするため、AwakeやStartではなくOnEnableハンドラがシーン初期化に相当する、という制約があります。

シーンオブジェクトのようにゲーム全体を通して存在するものは、Awakeの中では他のオブジェクトに依存しない初期化処理を書いて、OnEnableの中で他のコンポーネントへの関連付けやシーン初期化処理を記述する、という分担になります。
一方、演出のオブジェクトのような、プリロードしておいて適宜再生するものは初期化の流れがややこしかったです。
まず、PlayableDirectorで行われるTimeline(PlayableGraph)の生成は、生成時点の情報を使ってインスタンスが生成されその後更新されません。さらにPlayableDirectorの実行順がユーザスクリプトよりも先になります。そのため、ユーザスクリプトでのTimelineの初期設定を再生に反映させるためには、Startの前の実行順となるOnEnableかAwakeの中に書く必要がありました。
ただしOnEnableの中で初期設定をすると、プリロードでのインスタンス生成時と実際の再生時の2回実行されてしまいます。一方で、Awakeの中で初期設定すると、プリロードの時点で一度だけ実行されるため、実際の再生時の情報を反映させることができません。

結局、演出オブジェクトの初期化処理をOnEnableハンドラとは別のメソッドに分け、シーンオブジェクト内でのインスタンス生成時に初期化処理を呼び出すことにしました。演出固有のパラメータ変更は共通の初期化処理の後に呼び出します。
総じて、Startの前にほぼ決着するようなコーディングになりました。先制行動キャラ同士が戦うような感じです。

状態遷移器の自動生成

シーンオブジェクトはOnEnableの時点で演出オブジェクトの初期設定や再生を実行するのですが、Updateステップに入った後にもSignal受信に対して戦闘ステップごとに行う処理を切り替えます。その切替は状態遷移を使用して実装します。
ここで戦闘中の演出で生じるステップの数は、少なくとも 複数ラウンド×複数人数×複数の演出ステップ の分だけあります。それらのステップごとの状態遷移に関して、二つのアプローチがあります。
・単純に全ステップを状態遷移する状態遷移器を生成する
・プログラミング的に反復処理として遷移処理を書けるようにする
今回は戦闘ログを再生するだけのビューとして戦闘シーンオブジェクトを設計しているので、必要なステップは戦闘ログにすべて載っています。そのため、現時点では前者の方法をとります。状態遷移の数珠つなぎのお化けが生まれるので、メモリ使用量が多すぎたらプログラミング的な方法も考えます…

状態遷移の数珠つなぎのお化け。戦闘が終了するまで延々とつながる

状態遷移器の生成自体は難しくなく、状態オブジェクトを生成して遷移でつなぐだけです。これを戦闘ログの記述に従って行うことになります。生成したインスタンスを状態遷移器内のラムダ式が保持してくれるので、Exitアクションに破棄処理を書いておけばインスタンス管理も多少楽です。
Signal受信時の遷移の実装では、状態遷移器側では遷移させず、UnityのSignal Receiverから強制的に遷移させることにしました。

シーンオブジェクトのSignal Receiverの設定。Signal受信時に状態遷移用のメソッドを実行

PlayableDirectorへの動的バインディング

演出再生時に決まるデータをTimelineに与えるため、演出再生時のタイミングでPlayableDirectorにデータを登録(バインド)する必要があります。具体的には以下のデータをバインディングします。
・Signalを受け取るシーンオブジェクトをSignal Trackにバインディング
・行動者などをTimelineで動かすため、行動者のTransformを演出上のトラックにバインディング
・Timeline内で参照するための行動者と対象のTransformを指す変数に、実際の値をバインディング

演出のTimeline設定。
左下の「なし」のトラックがSignal Track、StartのトラックがStart(行動者)からEnd(対象)へ動くトラック。この2本のトラックに所定のGameObjectを動的バインディングする。
真ん中のトラックは"Actor"という固定の名前を持っていて(右上のインスペクタ)、この名前を検索してバインディングする。

各トラックへのバインディングはPlayableDirector.SetGenericBinding()を使います。
値のバインディングは以前実装したカスタムトラックへの設定です。これを使ってクリップ上で動きの地点を再生時に指定します。

注意として、バインディングでは参照がそのまま渡されます。なのでシーン側のTransformを上書きさせないために、演出側にGameObjectを持たせて値(構造体)をコピーする必要がありました。マーカー代わりのStart,Endオブジェクトは要らないかなと思ったら要りました。
それとトラックを特定するのに決まった名前が必要です。これは規約で決めておく必要があります。

UnityEventを使ったコールバック実行

演出用に個別の設定をしたい時に、実行タイミングの都合上イベントハンドラでは書けないので、メソッドを演出オブジェクト内のコンポーネントから読んで別途実行する必要があります。Signalのハンドラ設定のようにGUIでコールバックを設定する方法を探すと、UnityEventというクラスを使う方法がありました。

これを使って演出固有のコールバックをGUIから登録できるようにしました。ちなみに下のSingle Attack~のスクリプトでは移動の終点を変更し、行動者が行動対象へ向かって少しだけ動くようにしています。

演出オブジェクトのコンポーネント。Reference Value Setter(仮)でTimelineに与えるパラメータと、演出固有のパラメータ変更用のメソッド(Modify)を指定

その他

行動のエフェクトにはParticle Systemを使っています。ParticleもGameObjectと同様にTimelineのトラックで動かせます。自作のカスタムトラックでも普通に操作できました。
Particleの初期位置も上記のModifyの中で設定しています。行動者でも対象でもない演出内部のオブジェクトは演出固有のスクリプトで設定する、という方針です。
あと「続行/撤退」ウィンドウの表示ではマウスクリックして遷移しているのですが、有効シーンの切り替えには状態遷移を使い、フェードアウトの表示処理はDOTweenを使っています。

次の予定

戦闘シーンでの行動演出を再生する目途が立ったので、次はホームシーン周りを実装したいです。

この記事が気に入ったらサポートをしてみませんか?