Unity の Timeline で任意の処理を行うカスタムトラックを作るための世界一わかりやすい解説記事です

# はじめに
* Unity の Timeline に連動して任意のフレームで任意の処理を実行するためにカスタムトラックを実装する方法を説明します。

# なにをつくるか
* この記事では例として、Timeline のクリップでスタート地点とゴール地点を持ち、クリップの開始時にスタート地点にいて終了時にゴール地点まで移動する機能を実装します。

# 作成するファイルとその役割
* カスタムトラックを実装するために以下の五つのファイルを作成します

|ファイル名|役割|
|:-:|:-:|
|ExampleTrack|トラックの拡張クラス。主にカスタムトラック全体の設定を保持する役割を持つ|
|ExampleClip|クリップの拡張クラス。主に個々のクリップ単位での設定を保持する役割を持つ|
|ExampleBehaviour|単一のクリップの処理の実装クラス。タイムライン実行時に呼び出されるメソッドを持つ|
|ExampleMixerBehaviour|複数クリップを取りまとめる処理の実装クラス。タイムライン実行時にExampleBehaviourの後に呼び出され、全てのクリップの結果をまとめるメソッドを持つ|
|ExampleComponent|普通のMonoBehaviourコンポーネント。カスタムトラックのバインディングオブジェクトとして指定され、ExampleMixerBehaviourから呼び出される形でカスタムトラックと連携する|

# 各ファイルごとの解説
* それぞれのファイルごとに細かい解説と実装コードを掲載します。

## ExampleTrack
* トラックの拡張クラスです。
* この拡張クラスによって、クリップのクラスやバインディングオブジェクトのクラス、ミキサーのクラスなどカスタムトラックにかかわる全体的な環境の定義がなされます。
```CS
using UnityEngine.Timeline;
using UnityEngine;
using UnityEngine.Playables;

// ExampleTrack は Timeline のカスタムトラック用のクラス
[TrackColor(0, 0.8623f, 0.87f)] // Timeline ウィンドウで表示されるトラックの色
[TrackClipType(typeof(ExampleClip))] // クリップのクラス
[TrackBindingType(typeof(ExampleComponent))] // バインディングオブジェクトのクラス
public class ExampleTrack : TrackAsset
{
    // ミキサーの初期化関数
    public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
    {
        // ミキサーのクラスを指定
        return ScriptPlayable<ExampleMixerBehaviour>.Create(graph, inputCount);
    }
}
```

## ExampleClip
* クリップの拡張クラスです。
* 主に個々のクリップ単位での設定を保持する役割を持ちます。
* 具体的にはクリップごとにスタート地点とゴール地点を指定できるように後悔プロパティが用意されています。
* また、タイムライン再生時に処理を実行するアクターである Behaviour の初期化も行います。
```CS
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

// ExampleClip は Timeline のカスタムトラックで利用されるクリップ用のクラス
// アノテーションの fileName はアセットファイルとして保存する場合の名前
// menuName はクリップ作成の際のメニュー上の表示名
[CreateAssetMenu(fileName = "ExampleClip", menuName = "Timeline/ExampleClip")]
public class ExampleClip : PlayableAsset, ITimelineClipAsset
{
    // インスペクターから設定できる項目
    // startPosition で開始位置の指定
    // endPosition で終了位置の指定
    public Vector3 startPosition;
    public Vector3 endPosition;
    
    // クリップがブレンド可能であることを設定する
    public ClipCaps clipCaps => ClipCaps.Blending;

    // クリップの設定を使って処理を実行する Playable を具象化するメソッドの実装
    public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    {
        // 処理の実装が定義された Behaviour を取得する
        var playable = ScriptPlayable<ExampleBehaviour>.Create(graph);
        var behaviour = playable.GetBehaviour();

        // クリップのパラメータを Behaviour に渡す
        behaviour.startPosition = startPosition;
        behaviour.endPosition = endPosition;
        
        return playable;
    }
}
```

## ExampleBehaviour
* 単一のクリップの処理の実装クラスです。
* ProcessFrame メソッドはタイムライン再生時に MonoBehaviour.Update のように繰り返し呼び出されます。
* ProcessFrame の後に ExampleMixerBehaviour.ProcessFrame が呼び出されて結果がまとめられるので、ここでは前処理だけを行っています。
```CS
using UnityEngine;
using UnityEngine.Playables;

// ExampleBehaviour はクリップで実行する処理の実装クラス
public class ExampleBehaviour : PlayableBehaviour
{
    // startPosition はクリップで設定されたものが引き継がれる
    public Vector3 startPosition;
    // endPosition はクリップで設定されたものが引き継がれる
    public Vector3 endPosition;
    // currentPosition は Behaviour の処理結果を保持する
    public Vector3 currentPosition;
    
    // ProcessFrame はタイムライン再生時に実行されるメソッド
    // MonoBehaviour で言う Update のようなもの
    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        // クリップの全期間の取得
        double duration = playable.GetDuration();
        // 再生中の時間の取得
        double time = playable.GetTime();
        // クリップの進捗
        float progress = (float)(time / duration);

        // このクリップがバインディングオブジェクトの位置を startPosition から endPosition の間で移動させるものなので
        // クリップの進捗度から現在の位置を割り出して currentPosition に保存する
        currentPosition = Vector3.Lerp(startPosition, endPosition, progress);
    }
}
```

## ExampleMixerBehaviour
* 複数クリップを取りまとめる処理の実装クラスです。
* タイムライン再生時に ExampleBehaviour の後に呼び出され、再生フレームに存在する全てのクリップの結果をまとめる処理を行います。
```CS
using UnityEngine;
using UnityEngine.Playables;

// ExampleMixerBehaviour は Behaviour をミックスして最終的に ExampleComponent の処理を呼び出すクラス
public class ExampleMixerBehaviour : PlayableBehaviour
{
    // Behaviour と同様にタイムライン再生時に継続的に実行されるメソッド
    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        // 処理を実行する ExampleComponent の取得
        // playerData はバインディングオブジェクトであり、これをキャストして取得する
        var component = playerData as ExampleComponent;
        if (component == null)
        {
            Debug.LogError("component not found");
            return;
        }
        
        // 複数クリップが同一フレーム上に定義されている場合、それぞれの処理への反映度が Weight として渡される
        // totalWeight はこれを足し合わせて全体量を保持する
        float totalWeight = 0f;
        // tempPosition は各クリップの currentPosition を足し合わせて保持する
        Vector3 tempPosition = Vector3.zero;
        
        // 同一フレーム上のクリップ数を取得
        int inputCount = playable.GetInputCount();
        // 各クリップごとに処理
        for (int i = 0; i < inputCount; i++)
        {
            // クリップの Weight を取得
            var inputWeight = playable.GetInputWeight(i);
            // クリップの Behaviour を取得
            var input = (ScriptPlayable<ExampleBehaviour>)playable.GetInput(i);
            ExampleBehaviour behaviour = input.GetBehaviour();
            
            // Behaviour 側での現在のフレームでの currentPosition は計算済みになるので Weight を掛けたクリップの座標を足し合わせる
            tempPosition += behaviour.currentPosition * inputWeight;
            totalWeight += inputWeight;
        }

        if (totalWeight > 0)
        {
            // 全クリップの position を足し合わせて総 Weight で割ることで、現在の位置を割り出す
            var currentPosition = tempPosition / totalWeight;
            // ExampleComponent にこの位置を渡して処理を行う
            component.SetPosition(currentPosition);
        }
    }
}
```

## ExampleComponent
* 普通の MonoBehaviour コンポーネントです。
* カスタムトラックのバインディングオブジェクトとして指定され、ExampleMixerBehaviour から呼び出される形でカスタムトラックと連携します。
```CS
using UnityEngine;

// ExampleComponent はカスタマイズされたトラックから呼び出されて処理を実行するクラス
public class ExampleComponent: MonoBehaviour
{
    // ここでは例として Vector3 座標を渡されて、それを自身の transform にセットするという処理を行う
    public void SetPosition(Vector3 position)
    {
        transform.position = position;
    }
}
```

# つかいかた
* Timeline の左上のプラスアイコンから ExampleTrack を追加します
* 任意の GameObject に ExampleComponent をアタッチします
* 追加した GameObject を ExampleTrack にバインディングします
* トラックの任意のフレームを右クリックし ExampleClip を追加し、スタート位置とゴール位置を設定します
* Timeline を再生し動作を確認します