Unity 2017.1 と同時に Timeline がリリースされて以降、数多くのフィードバックをお寄せいただきました。Unity では多くのデベロッパーの方々との話し合いやフォーラムでのユーザーの皆様とのやり取りを通して分かったのは、多くの皆様が、Timeline を単なるシーケンシングツール以上のものとして使用したいと考えているということです。私はこれに関して今までにいくつかの講演(Unite Austin 2017 など)を行い、一部のユーザーの方々に向けて Timeline の使い方を説明した記事もブログにて発表しました。本記事では、Timeline を使用してダイアログを制御したり、分岐に対応したり、さらに Timeline をゲームの AI システムと接続する方法をご紹介します。
Timeline は元々、拡張性を持たせることを大きな目標として開発されました。この機能を設計したチームは、ユーザーの皆様が「組み込みのものだけでなく独自のクリップやトラックをビルドしたい」というニーズを持つことを常に念頭に置いていました。このため当然ながら、Timeline のスクリプティングに関して多くの質問が寄せられています。Timeline の基盤となっているシステムは強力なものですが、同機能に慣れていない方にとっては、扱いが難しくなることもあります。
しかし、まず Timeline とは何でしょうか?Timeline は、アニメーションクリップ、音楽、サウンドエフェクト、カメラショット、パーティクルエフェクト、さらに他の Timeline などの、異なる要素をシーケンスするためのリニア編集ツールです。基本的には Premiere®、After Effects®、Final Cut® などのツールと非常に似ていますが、リアルタイム再生用に開発されている点が異なっています。
Timeline についての基本的な解説は、Unity マニュアルの Timeline に関するドキュメンテーションをご覧ください。本記事では、この機能の基本的な概念をさらに一歩踏み込んでお話して行きます。
Timeline は Playables API をベースに実装されています。
これは、アニメーションやオーディオなどの複数のデータソースを読み出し、ミックスしてひとつの出力を通して再生できる強力な API です。このシステムは、精密なプログラムによる制御が可能で、オーバーヘッドが低く、パフォーマンスに焦点を当てて設計されています。ちなみに骨組みは、Animator コンポーネントを動かすステートマシンの背後にあるものと同じですので、Animator 用にプログラミングされたことがあるなら、聞き慣れた概念が登場するでしょう。
Timeline の再生が始まると、Playables と呼ばれるノードで構成されたグラフがビルドされます。これは PlayableGraph と呼ばれる、ツリーのような構造内に整理されます。
(注)シーン内の PlayableGraph(Animator、Timelines など)のツリーを視覚化したい場合は PlayableGraph Visualizer というツールがダウンロード可能です。本記事ではこのツールを利用して各種カスタムクリップのグラフを視覚化して行きます。Playable API とグラフについての(Animator との関連における)詳細は、Pierre-Paul のブログ記事をご参照ください。
ここからは、3 つの単純な例を使って Timeline の拡張方法をご紹介して行きます。まず基礎的な事項として、Timeline にスクリプトを追加する最も簡単な方法からご説明します。その後で他の概念にも順次触れて行き、最終的には大部分の機能をご活用いただけるようにしたいと思います。
本記事で使用する例をすべて含む小さなデモプロジェクトをパッケージにしました。ぜひダウンロードして参照しながら本記事をお読みください。もちろん、本記事だけでもお楽しみいただけますが。
(注)アセットは、異なる接頭語を付けることで例毎に区別しています(「Simple_」「Track_」「Mixer_」 など)。下記のコード内では読み易くするためにこれらの接頭語は省略しています。
最初の例は非常に単純です。目的は Light コンポーネントの色と強度をカスタムクリップで変更することです。カスタムクリップを作成するには、以下の 2 つのスクリプトが必要です。
Playable API の基本的な原則のひとつはロジックとデータの分離です。このためまず PlayableBehaviour を作成し、その中に以下のように行いたい事を記述する必要があります。
public class LightControlBehaviour : PlayableBehaviour { public Light light = null; public Color color = Color.white; public float intensity = 1f; public override void ProcessFrame(Playable playable, FrameData info, object playerData) { if (light != null) { light.color = color; light.intensity = intensity; } } }
何が行われているのでしょうか?まず、Light のどのプロパティを変更したいかに関する情報があります。また、PlayableBehaviour はオーバーライド可能な ProcessFrame というメソッドを持っています。
ProcessFrame は更新の度に呼び出されます。このメソッドの中に Light のプロパティを設定できます。PlayableBehaviour 内でオーバーライド可能なメソッドのリストはこちらです。次に、カスタムクリップ用の PlayableAsset を作成します(以下)。
public class LightControlAsset : PlayableAsset { public ExposedReferencelight; public Color color = Color.white; public float intensity = 1.0f; public override Playable CreatePlayable (PlayableGraph graph, GameObject owner) { var playable = ScriptPlayable .Create(graph); var lightControlBehaviour = playable.GetBehaviour(); lightControlBehaviour.light = light.Resolve(graph.GetResolver()); lightControlBehaviour.color = color; lightControlBehaviour.intensity = intensity; return playable; } }
1 つの PlayableAsset には 2 つの目的があります。1 つ目には、これは Timeline アセット自体の中でシリアライズされるため、クリップデータを含んでいます。2 つ目には、Playable グラフ内に入る PlayableBehaviour をビルドします。
最初のラインをご覧ください。
var playable = ScriptPlayable.Create(graph);
これが新しい Playable を作成し、それにカスタム挙動である LightControlBehaviour をアタッチします。その後 PlayableBehaviour のライトのプロパティを設定できます。
ExposedReference はどうでしょう?PlayableAsset はアセットなので、シーン内のオブジェクトを直接参照することは不可能です。ExposedReference は「CreatePlayable が呼び出された時にはオブジェクトが 1 つリゾルブされる」ことを保証する機能を果たします。
これで、Timeline に Playable Track を追加し、その新しいトラック上で右クリックしてカスタムクリップを追加することができます。クリップに Light コンポーネントをアサインすると結果を確認できます。
この例では、組み込みの Playable Track は、今作成したような単純な Playable トラックを受け取れる汎用トラックです。より複雑な状況になるとクリップを専用トラックにホスティングする必要があります。、
最初の例に関して注意すべき点は、カスタムクリップを追加する度に各クリップに Light コンポーネントをアサインする必要があり、これが沢山あると骨の折れる作業になることです。これは、トラックのバウンドオブジェクトを使うことで解決できます。
トラックは、それに紐付けられた(「バウンド」)オブジェクトまたはコンポーネントを 1 つ持つことができます。つまり、トラック上の各クリップが、そのバウンドオブジェクト上で直接実行可能になります。これは非常に一般的な挙動であり、アニメーショントラックやアクティベーショントラック、Cinemachine トラックはこの仕組みで機能しています。
あるライトのプロパティを複数のクリップで修正したい場合は、Light コンポーネントをバウンドオブジェクトとして求めるカスタムトラックを作成します。カスタムトラックを作成するには TrackAsset を拡張するスクリプトが別に 1 つ必要です(以下)。
[TrackClipType(typeof(LightControlAsset))] [TrackBindingType(typeof(Light))] public class LightControlTrack : TrackAsset {}
ここには 2 つの属性があります。
PlayableAsset と PlayableBehaviour も、トラックと連動させるために若干修正する必要があります。ご参考のために、もう必要なくなっているラインをコメントアウトしました。
public class LightControlBehaviour : PlayableBehaviour { //public Light light = null; public Color color = Color.white; public float intensity = 1f; public override void ProcessFrame(Playable playable, FrameData info, object playerData) { Light light = playerData as Light; if (light != null) { light.color = color; light.intensity = intensity; } } }
PlayableBehaviour には、もう Light 変数は必要ありません。ここでは、ProcessFrame メソッドがトラックのバウンドオブジェクトを直接提供しています。あとは、適切なタイプにオブジェクトを投げるだけで済みます。これはいいですね!
public class LightControlAsset : PlayableAsset { //public ExposedReferencelight; public Color color = Color.white; public float intensity = 1f; public override Playable CreatePlayable (PlayableGraph graph, GameObject owner) { var playable = ScriptPlayable .Create(graph); var lightControlBehaviour = playable.GetBehaviour(); //lightControlBehaviour.light = light.Resolve(graph.GetResolver()); lightControlBehaviour.color = color; lightControlBehaviour.intensity = intensity; return playable; } }
PlayableAsset は、もう Light コンポーネント用の ExposedReference をホールドする必要はありません。この参照はトラックによって操作され、直接 PlayableBehaviour に与えられます。
タイムライン内に LightControl トラックを追加し、それに特定のライトを紐付け(バインド)することができます。そのトラックに追加した各トラックは、同トラックに紐付けされた Light コンポーネントを操作します。
Graph Visualizer を使ってこのグラフを表示すると、こんな感じになります。
予想通り、右側の 5 つのボックス(クリップ)が 1 つのボックスに収束しています。この 1 つのボックスがトラックです。その後、すべてが Timeline(紫のボックス)に入ります。
(注) ピンク色の「Playable」というボックスは、実は Unity が自動で作成するミキサー Playable です。このため、クリップと同じ色になっています。ミキサーとは何でしょうか?次の例でミキサーについてお話します。
Timeline では、重なり合うクリップ同士をブレンドあるいはクロスフェードさせることができます。カスタムクリップもブレンドが可能です。ただし、これを行うにはブレンドされるすべてのクリップのデータにアクセスできるミキサーを 1 つ作成する必要があります。
ミキサーは、上記で使用した LightControlBehaviour 同様、PlayableBehaviour から派生します。そしてここでも ProcessFrame 関数を使用します。主な違いは、この Playable は、トラックスクリプトによって(関数 CreateTrackMixer をオーバーライドすることで)ミキサーとして明示的に宣言されていることです。 LightControlTrack スクリプトは、以下のようになりました。
[TrackClipType(typeof(LightControlAsset))] [TrackBindingType(typeof(Light))] public class LightControlTrack : TrackAsset { public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount) { return ScriptPlayable.Create(graph, inputCount); } }
このトラック用の Playable Graph が作成されると、新しい挙動(ミキサー)も作成されてトラック上のすべてのクリップに接続されます。
またロジックも PlayableBehaviour からミキサーへ動かします。結果、PlayableBehaviour は以下のようにシンプルになりました。
public class LightControlBehaviour : PlayableBehaviour { public Color color = Color.white; public float intensity = 1f; }
含まれているのは、ランタイムでPlayableAsset から来るデータのみです。一方ミキサーは、以下のように、その ProcessFrame 関数内にすべてのロジックを含むことになります。
public class LightControlMixerBehaviour : PlayableBehaviour { // (注)この関数はランタイムおよびエディットタイムで呼び出されます。プロパティの値を設定する際は、このことを念頭に置いてください。 public override void ProcessFrame(Playable playable, FrameData info, object playerData) { Light trackBinding = playerData as Light; float finalIntensity = 0f; Color finalColor = Color.black; if (!trackBinding) return; int inputCount = playable.GetInputCount (); //このトラック上の全クリップ数を取得する for (int i = 0; i < inputCount; i++) { float inputWeight = playable.GetInputWeight(i); ScriptPlayableinputPlayable = (ScriptPlayable )playable.GetInput(i); LightControlBehaviour input = inputPlayable.GetBehaviour(); // 上記の変数を使用してこの Playable の各フレームを処理する。 finalIntensity += input.intensity * inputWeight; finalColor += input.color * inputWeight; } //バウンドオブジェクトに結果をアサインする trackBinding.intensity = finalIntensity; trackBinding.color = finalColor; } }
ミキサーは、トラック上に存在するすべてのクリップにアクセスすることができます。この場合、現在ブレンドに寄与するすべてのクリップの Intensity と Color の値を読み込まなければならないので、for ループでそれらを順次処理する必要があります。1 回のサイクルごとに入力(GetInput(i))にアクセスし、その入力に各クリップのウェイト(GetInputWeight(i))を掛け合わせた値を足し合わせていくことで、そのクリップがブレンドに寄与する度合いを取得します。
2 つのクリップがブレンドされているとしましょう。1 つは赤に、もう 1 つは白に寄与しています。ブレンドが 4 分の 1 進んだ時点では、色は 0.25 * Color.red + 0.75 * Color.white で、若干フェードした赤になっています。
ループが終了すると、紐付けされた Light コンポーネントに合計値を適用します。これは以下のような結果になります。
赤いボックスは、プログラムした通りのミキサー Playable になっており、フル制御が可能です。これは上述の(ミキサーが Unity デフォルトのものであった)例 2 とは対照的です。
また、ブレンド途中のグラフなので、緑のボックス 2 と 3 の両方にミキサーに接続する鮮明な線が付いていることにもご注目ください。これは、これらそれぞれのウェイトが 0.5 前後であることを示しています。
ミキサー内にブレンドを実装する場合、どんなロジックにするかはあなた次第です。2 つの色のブレンドは単純ですが、例えば極端な例で、AI システム内の異なる AI ステートを表す 2 つのクリップのブレンドはどうでしょう?UI 内の 2 行のダイアログは?2 つの静的ポーズを 1 つのストップモーション・アニメーションにブレンドする場合は?あるいは、連続的なブレンドではなく、飛び飛びの値を取るブレンド(ポーズ同士がモーフィングされるが、それが 0・0.25・0.5・0.75・1 など離隔した値の増加で行われる)を行う場合もあるでしょう。
この強力なシステムがあれば、できることは無限大です!
本ガイドの最後のステップです。上述の例に戻り、「テンプレート」と呼ばれるものを使って、データを動かすための異なる方法を実装してみましょう。この方法の大きな利点のひとつは、テンプレートのプロパティをキーフレームできることです。このおかげで、カスタムクリップ用のアニメーションを直接 Timeline 上で作成することが可能となります。
上述の例の中では、Light コンポーネントへの参照があり、PlayableAsset と PlayableBehaviour の両方の Color と Intensity がありました。データはインスペクター内の PlayableAsset で設定され、その上でグラフ作成時にそれがランタイムで PlayableBehaviour 内にコピーされていました。
これも有効な方法ですが、データが重複され、常にそれらの同期を維持しなければならなくなります。これでは間違いが起こり易くなります。代わりに、PlayableBehaviour の「テンプレート」という考え方を用いることも可能です。これを行うには PlayableAsset 内でそれへの参照を作成します。したがって、まず以下のように LightControlAsset をリライトしてください。
public class LightControlAsset : PlayableAsset { public LightControlBehaviour template; public override Playable CreatePlayable (PlayableGraph graph, GameObject owner) { var playable = ScriptPlayable.Create(graph, template); return playable; } }
LightControlAsset は、値自体ではなく LightControlBehaviour だけを持つ状態になりました。以前よりさらにコードの量が少なくなりました!
以下のように、LightControlBehaviour は変更しないままにしてください。
[System.Serializable] public class LightControlBehaviour : PlayableBehaviour { public Color color = Color.white; public float intensity = 1f; }
Timeline 内でクリップを選択するとテンプレートへの参照によって以下のインスペクターが自動的に生成されるようになりました。
このスクリプトが整備されたら、アニメーションが付けられます。新しいクリップを作成すると、トラックのヘッダに赤い円形ボタンが表示されます。これは、Animator を追加しなくてもクリップにキーフレームを追加できるようになったことを意味します。これは、赤いボタンをクリックし、クリップを選択し、キーを作成したい場所に再生位置を動かして、そのプロパティの値を変えるだけで行えます。
また、白いボックスをクリックすると、カーブビューが展開され、キーフレームによって作られた曲線を確認できます(下図参照)。
もう一つ便利な機能があります。Timeline クリップ上をダブルクリックすると Animation パネルが開いてそれが Timeline にリンクされます。リンクされると以下のボタンが表示されます。
リンクされれば、再生位置マーカ―をTimeline ウィンドウと Animation ウィンドウの両方で同時に動かせます。同期が維持されますので、キーフレームのフル制御が可能です。Animation ウィンドウ内でアニメーションが修正できるので、キーフレームの操作がより快適になります。
このビューでは、アニメーションカーブとドープシートをフルに活用し、カスタムクリップのアニメーションの精密な調整が行えます。
(注)この方法でアニメーションを付ける場合、アニメーションクリップが作成されます。これはTimeline アセットの配下にあります。
Timeline をスクリプティングでレベルアップさせることで開ける無限の可能性を、本記事でご紹介できたなら嬉しく思います。
ぜひ、ご質問やフィードバックを Twitter にお寄せください。また、Timeline を使った皆様の制作物も是非ご紹介ください!