土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

#unity Timelineからシグナルを使ってAnimationController制御する

 この記事は「Unity Advent Calendar 2022 その1」の9日目の記事になります。
qiita.com

 前日は@YamadaGamesさんの「Unity Cinemachine VirtualCameraを知らない人向けだけの紹介」でした。
qiita.com

 今回は、スクリプトを書かずに、Timelineからシグナルを使ってAnimationControllerを制御する方法を紹介します。

やる事になった経緯

 土屋には仲良くさせて頂いている映像作家さんがいます。その映像作家さんは、Unityを使ってキャラモデルのアニメーションを録画する形で商用映像を作っており、土屋は不定期に技術的な相談を受けていました。今回の相談は「AnimationContollerを設定したキャラモデルをTimelineから制御できないか」という物でした*1

 Unityでキャラモデルをアニメーションさせる方法は、大まかにAnimationContollerによるステートマシンでの制御と、Timelineによる時間軸での制御に大別されるかと思います*2。ゲーム中にプレイヤーからのインタラクションに応じてアニメーションを切り替える必要がある時は前者、カットシーンのようにインタラクションが無く、複数のオブジェクトが緊密に同期する場合は後者を使います。

 映像作家さんは普段Timelineで作業をしていますが、今回は「ジャンプ」「手を振る」などの短い汎用モーションを適当なタイミングで再生するモブキャラを登場させようと考えていました。この汎用モーション群をAnimationContollerで管理し、どのモーションを再生するかをTimelineから指定出来ないかという相談を頂いたわけです*3

 AnimationContollerには、パラメータという必要に応じて追加出来る変数のような機能があります。外部からパラメータに値を書き込むと、それをトリガーとしてステートを切り替える事ができます。なのでTimeline上の任意タイミングでキャラモデルのAnimationContollerのパラメータを更新できれば良いわけです。今回はこの処理をシグナルで実現します。

シグナルとは

 シグナル(Signal)は、Timelineが提供しているマーカー(Marker)という機能を汎用的に使えるようにした物です。

 マーカーは、Timeline上の任意のタイミングでスクリプトを実行する為の機能です。シグナルは公式によるマーカーのカスタム実装で、予め対象のコンポーネント/メソッド/引数*4を設定しておくと、任意のタイミングでそのメソッドを呼びだす事ができます。
docs.unity3d.com
 Signalについては公式ドキュメントが用意されていないようです*5

 余談ですが、マーカーをカスタム実装した記事も書いてますのでご興味ある方はこちらもどうぞ。
someiyoshino.info

実際にやってみる

 作業としては以下の3段階に分かれます。今回はスクリプトを書かずに実現しています。

  1. アニメーションコントローラーの設定
  2. シグナルレシーバーとシグナルトラックの設定
  3. シグナルの登録

1:アニメーションコントローラーの設定

 まず、AnimationContollerアセットのおさらいから。
 このアセットはキャラモーションの遷移をステートマシンで制御する物で、キャラモデルにアタッチされたAnimatorコンポーネントから参照されています。

 ステートマシンは「ステート(状態)」と「トランジション(遷移)」の2個の組み合わせから構成されていて、それぞれについて設定が必要です。
 また、その他に、外部からステートマシンを制御するために「パラメータ」の設定が必要です。

ステートの設定

 ステートは、エディタ上では長方形で示され、それぞれが一個のモーション*6を表します。オレンジ色のステートはデフォルトで用意されている物で削除できません。実行状態になるとまずこのオレンジ色のステートに遷移します(通常は待機モーションをループさせたりしてます)。

 モーションデータをドラッグしたり、右クリックで新規作成すると、新たなステート(灰色の長方形)が追加されます。

 ステートのインスペクタはこんな感じになっています。

 今回、インスペクタの個々の説明は省略します(いずれ別記事で改めて書きます)。"Parameter"というチェックボックスを持つプロパティが4つありますが、これらは、後述するパラメータを通じて外部から値を設定できます(「パラメータ」の項で補足します)。

トランジションの設定

 トランジションはステート間の遷移条件を表し、エディタ上では矢印で示されます。あるステートから他のステートにモーションを切り替えるために、ステート同士をトランジションで接続します。それぞれのトランジションには遷移条件を設定し、その遷移条件が満たされるとステート(≒モーション)が切り替わります。

 矢印をクリックすると、ステートと同じようにインスペクタにプロパティが表示されます(こちらも詳細説明は省略)。矢印が小さく線も細いので、慣れないとトランジションをクリック出来る事に気づかないかもしれません。なんでこんなデザインなんだこれ?

 閑話休題。ちなみになんですが、トランジションのインスペクタには、ステート遷移中のアニメーションブレンド比率をビジュアライズするトランジショングラフが表示されています。

 このグラフを直接操作して値を更新する事も出来るのですが、UIの挙動に強いクセがあるため、基本はプロパティを直接操作した方が直感的だと思います。

 個々のトランジションでは、矢印の元のステートから矢印の先のステートへ遷移する条件を設定できます。設定するにはConditionsの「+」をクリックします。

 Conditionsには設定済みのパラメータのいずれかを指定します。パラメータについては次項で説明します(実際の設定もそこでやります)。

パラメータ

 AnimationControllerは、外部からステートマシンを制御する為に「パラメータ」という機能を使用します。これは特殊な変数のような物で、用途に応じて必要な型のパラメータを任意個数用意します。

 パラメータを設定するには、Animatorウィンドウの左ペインを「Parameters」タブに切り替え*7、「+」をクリックしてリストから型を選択します。

 パラメータとして用意されている型はFloat(浮動小数)/Int(整数)/Bool(真偽)/Trigger(オン/オフ)になります。今回は扱いが楽*8なTrigger型のパラメータを作成します。
 追加されたパラメータをダブルクリックすると名称を変更できます。この名称を使って外部からパラメータにアクセスします。ここでは"Trigger1"にしておきます。

 次に、作成したパラメータを、トランジションのConditionsに設定します。


 これによって、"Trigger1"トリガーがオンになった時に矢印の先のステートに処理が遷移するようになります。
 Trigger型のパラメータは引数を持たないので、選択するだけで終わりですが、他の型であれば更に細かく条件を設定できます。1つのトランジションが複数の遷移条件を持つ事もできますし、1個のステートが複数のトランジションを持つこともできます。

2:シグナルレシーバーとシグナルトラックの設定

 シグナルの送信/受信を行うにはそれぞれについて作業が必要です。

  • 送信:Timelineにシグナルトラックを追加する。
  • 受信:Animatorを含むGameObjectにSignalReceiverコンポーネントを追加する。
送信:Timelineにシグナルトラックを追加する。

 シグナルトラックを追加するには、Timeline上で右クリックし、ドロップダウンリストからSignalTrackを選択します。

受信:SignalReceiverコンポーネントを追加する。

 Animatorを含むGameObject(通常はキャラモデルのroot)に、SignalReceiverコンポーネントをアタッチします*9

 更に、SignalReceiverコンポーネントをアタッチしたGameObjectを、シグナルトラックに参照として設定します。

 これによって、このGameObjectがシグナルの送信先になります。

3:シグナルの登録

 いよいよシグナルをシグナルトラック上に埋め込みます
 シグナルトラック上で右クリックし、Add Signal Emmiterを選択します。

 選択すると、上図のように、シグナルトラック上にマーカーが埋め込まれます。ところが、このままではまだ機能しません。
 実はSignalの本体はアセット(つまりリソースファイル)でして、今埋め込んだマーカーからアセットへの参照を設定する必要があるのです。これはつまり、参照先となるSignalアセットを作成する必要があるという事です。

 Signalアセットを作成するには、マーカーのインスペクタのEmit Signalプロパティで"Create Signal..."を選択し、ファイル名を設定して保存します。作成済みのSignalアセットがあれば、それを選択する事もできます。

 このような仕様になっているため、内容の異なるシグナルを使う場合にはそれぞれについてアセットの作成が必要です(内容が同じであればアセットを使い回せます)。

余談

 「設定の異なるシグナルごとにアセットを作成する必要がある」というのはちょっと頂けない仕様に感じます*10。シグナルが非常に強力なフレームワークにもかかわらず知名度が低いのはこの取り回しの面倒さがあるように思います。このような仕様になっているのはなにかしら理由があるんでしょうが、どうにかならんかなあ……。

シグナルの設定

 Signalアセットへの参照を設定したら、シグナルを追加出来るようになります。インスペクタでマーカーの名前の書かれた項目の「+」をクリックしてリアクション設定項目を増やします*11


 右上のペインでシグナルを送信する先のMonoBehaivior継承クラスと実行するメソッドを、右下に引数として渡す値を設定します。

 ここではAnimator.SetTrigger(string)を設定し、引数に"Trigger1"が渡るようにしました。スカラー型の引数を一個まで持つメソッドであればどれでも呼びだす事ができます。これはかなり強力な機能と言えるでしょう。

完了!

 以上で準備は終わりです。実行すると、Timelineのタイムカーソルがマーカーに到達したタイミングで、Animator.SetTrigger("Trigger1")が実行されます。それによってパラメータ"Trigger1"がオンになり、それがトリガーとなってステートが遷移し、アニメーションが実行されます。

終わりに(あるいは続編予告)

 シグナル/シグナルレシーバーは非常に強力なTimelineの機能ですが、あまり活用されている所を目にしません。実際使ってみると、マーカー置くたびにアセットを新規作成するのが面倒で「そりゃそうだ」となってしまいますが、Markerのサンプル実装としては非常に面白いのではないかと思います。

 また、脚注で何度か書きましたが、映像作家さんからの相談は今回の物よりももう一段複雑な物で、スクリプトを書かないと解決できない物でした。これについては別記事で紹介したいと思っています。できれば年内にアドカレ記事としてアップしたいと思っていますが、難しいかなあ……。

 UnityAC2022その1、明日は@nimushikiさんです。

補足:マーカーの実装についての別記事

 ユーザーがマーカー(Marker)をカスタマイズする事もできます。本文中にも触れましたが、以前土屋が作った記事も参照してください(今気づいたけど去年のアドカレだった)。
someiyoshino.info
 リンク先の記事では、マーカーの埋め込みにアセット追加は必要ありません(普通そうだろ*12)。

宣伝「Unityシェーダープログラミングの教科書シリーズ」

 土屋は同人誌「Unityシェーダープログラミングの教科書」シリーズを書いています。現在以下の5冊が出ています。

 これらの同人誌は、現在BOOTHの下記ストアにてPDF版を有料頒布しています。
s-games.booth.pm
 Unityのシェーダープログラミング関連について、世界で最も詳細な本だと自負しています。リンク先にサンプルページがあるので、是非御覧になってください。

参考リンク

今検索したら、テラシュールブログさんに良記事がありました。なぜこれを見逃した俺。
tsubakit1.hateblo.jp

*1:記事の流れの都合上、本来の相談をアレンジしています。本来の相談については、別の記事で改めて書こうと思っています

*2:CinemachineはTimeline系に入るものとする

*3:実は、この条件を満たすだけであればわざわざAnimationContollerを使わず、Timelineで直接モーションを指定すれば良かったりします。本当の相談はもっと複雑な物でした。これは別記事で。

*4:0個または1個

*5:Timelineが公式Package化する過程で混乱があったのかも。Markerについてはあります。

*6:モーションデータと考えて良いです

*7:ここ、UIデザインの関係でタブ切り替えである事が分かりにくいので注意

*8:ステートの遷移が終わると自動的にオフになってくれる

*9:実際には、SignalReceiverコンポーネントは任意の場所に配置できますが、シグナルの追加時に自動的にGameObjectを関連付けてくれるのでこの方が楽です

*10:それとも土屋がなにか勘違いしているのかな……

*11:一つのマーカーから任意個数のシグナルを送信できます

*12:シグナルはリフレクションを使ってるだろうから、そのあたりの問題なのかなあ?