simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

Unity:Timelineでスクリプトの関数を呼ぶ具体的な手順

昨今のゲームはアイドルが歌って踊るシーンを眺めたりするものが多く、そういったシーンを作るときにタイムラインを制作することになります。

各社が内製ツールで頑張っていたため、Unity 2017 で公式に導入されました。
docs.unity3d.com

私たちが知りたいのはタイムラインの表面的な編集方法ではなく、システムの根幹の設計思想なので、その部分を重点的にとらえてきたいと思います。

色々書いたのですが、最初にこのビデオを通しで見ることが重要なことがわかりました。(51分の動画です、全部見ました。)
www.youtube.com

動画から得られた知見:
スクリプト拡張には 3通り
1.Animation Track にて、疑似変数を MonoBehaviour に作成して、LateUpdate などで目的のスクリプトを実行(パラメータをアニメーションで完全に操作する場合に有効)
2.Control Track にて、ITimelineControl インタフェースを実装する MonoBehaviour を作成して、SetTime 関数内で目的のスクリプトを実行(シークバーの時間が double 型で取れるが、クリップ全体の長さが取得できないので注意)
実装イメージはこちらの記事がつかみやすい。
tsubakit1.hateblo.jp
3.カスタム Playable Track にて、PlayableBehabiour を実装し、ProcessFrame 関数内で目的のスクリプトを実行する(ただし Track と Clip それぞれで Behabior.ProcessFrame を利用するが、Clip の方処理落ちで呼ばれないことがあるので、使わないでください)
カスタム Playable Track でシークバーの時間を取る手段はありませんので PlayableDirector をシーンから見つけ出し time を参照してください。

■ここから導入
タイムラインを扱う時、最初に覚えなければならないのは Playable ~という用語の意味です。

まずは基本中の基本ですが、ゲーム実行中に条件がそろったタイミングでタイムラインが再生され、終了し、再びゲームに戻ってくる
このときに役割をこなすオブジェクトたちを見てみましょう。

Timeline はスクリプトから作成するときは

TimelineAsset timeline = ScriptableObject.CreateInstance<TimelineAsset>();

と書きます。ほか、プロジェクトビューのコンテキストメニューの Create > Timeline でもアセットファイルとして作成可能です。
Timeline を利用する時、必ずシーン内に作成するのが Playable Director です。

シーンに配置する場合は、Playable Director コンポーネントを追加するだけです。
f:id:simplestar_tech:20180923091629j:plain

Playable Director は再生する Timeline を一つだけ設定することができます。
ツリー構造で親から順に Timeline に登場する概念を並べると次のおとり
Playable Director(どこにでもいる) ≒ Timeline(Playable Director に配置) >Tracks (オブジェクト単位で平行世界軸方向に分散して配置) > Clips (イベント単位で時間軸方向に分散して配置)

指定したオブジェクトに何らかのイベントを働きかけるのは Clip ですが、それがいつ起きるのかは Clip の時間軸方向の配置場所によって決定され
どのオブジェクトに働きかけるかは、Track 単位で決まり、Track を無数に管理しているのが Timeline といったところです。

2週間ほど前に、Timeline と Cinemachine を統合したシーンを初めて触りました。その時の記事
simplestar-tech.hatenablog.com

Timeline の編集とは、オブジェクトに紐づくトラックを並列に追加し、そのうちの一つのトラックの中にクリップを順番に配置することだった

初見でもだいたい同じメンタルモデルを構築しているのがわかります。

さてゲーム中にこの Playable Director を任意の条件で再生するという方法はチュートリアルの動画に示されています。(8分ほどの動画です、最後まで見ました。)
www.youtube.com

動画から得られる知見:
Playable Director を実際にアニメーションするオブジェクトや、イベントを発行するオブジェクトなどに割り当てて
ディフォルトでチェックが入っている Play On Awake のチェックを外しておく

次のように、条件に合った時に Playable Director を Play してタイムラインを再生する

    [SerializeField]
    private PlayableDirector playableDirector;

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            playableDirector.Play();
        }
    }

基本中の基本(ゲーム実行中に条件がそろったタイミングでタイムラインが再生され、終了し、再びゲームに戻ってくる)
の具体的な手順がイメージできるようになりました。

動画に登場した Playable Asset について理解度を上げていきます。
参考にした記事はこちら

www.shibuya24.info

記事から得られた知見:
・Timeline にはいくつかのタイプの Track を追加することになりますが、Control Track は Prefab を Instantiate する Track でした。
公式ドキュメントには説明がない)
・特定のタイミングにおけるオブジェクトの操作、挙動を処理させるクラスを作る場合は PlayableBehaviour を継承して作る
・PlayableBehaviour が参照するオブジェクトを渡すのがTrackに相当する TrackAsset または PlayableAsset クラス

ここまでの Timeline の習熟作業でわかったこと
・Timeline に Track を追加する作業がある
・Track にゲームオブジェクトを割り当てる(バインディングする)作業がある
・Track に Clip を追加する作業がある

■本題の作業にフォーカス

スクリプトで Track と Clip を定義する仕組みが用意されています。
Track に相当するのが TrackAsset、Track, Clip 両方に相当するのが Playable Asset、Clip 内でのスクリプト処理を担当するのが PlayableBehaviour、複数の Clip を Mix するのが PlayableBehaviour です(混乱させてすみませんが、そういうことになっている)。

まずは空の Playable Asset を実装して Timeline に Track として追加してみましょう。手順は Create > Playables > Playable Asset C# Script を選択してクラスコードを生成します。

f:id:simplestar_tech:20180923110550j:plain

作られたコードは次の通り

using UnityEngine;
using UnityEngine.Playables;

[System.Serializable]
public class NewPlayableAsset : PlayableAsset
{
    // Factory method that generates a playable based on this asset
    public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
    {
        return Playable.Create(graph);
    }
}

実際、Timeline に追加できます。(Drag & Drop または Add メニューから選択可能)
f:id:simplestar_tech:20180923111359j:plain

混乱を招いていますが、Playable Asset は Track であり Clip でもあります。
要は Clip にはどうしても Track が必要なので、勝手に Playable Track が作られるというメンタルモデルを持つと納得できると思います。
最初に紹介した 50分の動画の発表者も、これの存在を忘れていて困っていましたね。

その Clip 内でスクリプト処理を担当する Playable Behavior を作ります。
手順は Create > Playables > Playable Behavior C# Script を選択します。

生成されるコードは次の通り

using UnityEngine.Playables;

// A behaviour that is attached to a playable
public class NewPlayableBehaviour : PlayableBehaviour
{
    // Called when the owning graph starts playing
    public override void OnGraphStart(Playable playable)
    {
        
    }

    // Called when the owning graph stops playing
    public override void OnGraphStop(Playable playable)
    {
        
    }

    // Called when the state of the playable is set to Play
    public override void OnBehaviourPlay(Playable playable, FrameData info)
    {
        
    }

    // Called when the state of the playable is set to Paused
    public override void OnBehaviourPause(Playable playable, FrameData info)
    {
        
    }

    // Called each frame while the state is set to Play
    public override void PrepareFrame(Playable playable, FrameData info)
    {
        
    }
}

このスクリプトは Timeline に Drop して使うものではありません。
先ほど作った Playable Asset のクラスにて new してインスタンスを渡して使います。
コードは以下の通り

using UnityEngine;
using UnityEngine.Playables;

[System.Serializable]
public class NewPlayableAsset : PlayableAsset
{
    // original code from
    // http://www.shibuya24.info/entry/timeline_basis
    public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
    {
        return ScriptPlayable<NewPlayableBehaviour>.Create(graph, new NewPlayableBehaviour());
    }
}

ゲームオブジェクトを参照したい場合は以下の通り
そろそろUnity2017のTimelineの基礎を押さえておこう - 渋谷ほととぎす通信 の記事を参考にしています。

[System.Serializable]
public class NewPlayableAsset : PlayableAsset
{
    public ExposedReference<GameObject> sceneObj;
    public GameObject projectObj;

    // original code from
    // http://www.shibuya24.info/entry/timeline_basis
    public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
    {
        var behaviour = new NewPlayableBehaviour();
        behaviour.sceneObj = sceneObj.Resolve(graph.GetResolver());
        behaviour.projectObj = projectObj;
        return ScriptPlayable<NewPlayableBehaviour>.Create(graph, behaviour);
    }
}

PlayableBehaviour

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;

// A behaviour that is attached to a playable
public class NewPlayableBehaviour : PlayableBehaviour
{
    public GameObject sceneObj;
    public GameObject projectObj;

    // Called when the owning graph starts playing
    public override void OnGraphStart(Playable playable)
    {
    }

    // Called when the owning graph stops playing
    public override void OnGraphStop(Playable playable)
    {
    }

    // Called when the state of the playable is set to Play
    public override void OnBehaviourPlay(Playable playable, FrameData info)
    {
        Debug.Log("Seek Bar Enter the Clip");
    }

    // Called when the state of the playable is set to Paused
    public override void OnBehaviourPause(Playable playable, FrameData info)
    {
        Debug.Log("Seek Bar Leave the Clip");
    }

    // Called each frame while the state is set to Play
    public override void PrepareFrame(Playable playable, FrameData info)
    {
        if (null != sceneObj.GetComponent<PlayableDirector>())
        {
            Debug.Log("PrepareFrame playableDirector.time = " + sceneObj.GetComponent<PlayableDirector>().time.ToString("0.000"));
        }        
    }
}

これで正しく動作することを確認できました。
動画にあったような、フレームが飛ぶ現象が考えられるというものがありましたが、それ恐怖ですね。
1フレームだけ設定した Clip が呼ばれずにすべてが破綻するようなシーンは作らないように、運用していかなければなりませんね(げんなり)

■まとめ
Timelineでスクリプトの関数を呼ぶ具体的な手順を示しました。
実際に動作確認も済ませました。
50分の動画では TrackAsset と Mixer の活用を推奨していましたが、ちょっと今回の記事に盛り込むにはボリュームありすぎでやってません。
PlayableBehaviour の OnBehaviourPlay は必ず呼ばれて欲しいなぁと思いますが、呼ばれないケースというものを体験するシーンを作るのも面倒くさくてやっていません。
おそらくタイムラインを活用する人たちは、タイミングバグに苦しむことだと思います。
案外 Activation Clip で Start 関数にスクリプトを仕込むだけの方が楽な手法なのかもと思いました。

最後に一言、Timeline のスクリプトカスタマイズは数十分でマスターできるもんじゃなかった…