# [Unity] CinemachineをTimelineのCustomTrackで制御してみた

## 経緯
UnityのCinemachineはCamera用の便利な制御モジュールです。ですが、OrbitalTransposerなどの機能を使うと制御コードを書く必要があります。今回は都度コードを書く手間を省くのと、アーティストさんが簡単にTimelineで制御できるようにCinemachineVirtualCameraのOrbitalTransposer用CustomTrackを作成してみました。

## Goal
![Timeline.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298907/16017310-df3f-8924-00c2-3434e8afb006.jpeg)
TimelineTrackにバインドしたVirtualCameraが対象物の周りをぐるぐる回るようなCustomTrackを作成します。CinemachineVirtualCameraを対象物の周りでぐるぐる回すのにOrbitalTransposerを使います。(OrbitalTransposerはVirtualCameraのBodyで選べます)

###◆ 環境
Unity2020.3
Cinemachine 2.5.0

## Timeline
UnityのTimelineはカットシーンなどのゲームプレイシーケンスを作るための機能です。ムービー編集ソフトではムービープレイシーケンスを編集しますが、それのゲーム版と思うと理解しやすいかもしれません。(もちろんゲーム以外にも活用できます)

## CustomTrack
UnityのTimelineに独自機能を載せるにはCustomTrackを作る必要があります。今回はUnityのDefaultPlayablesに沿ってCustomTrackを作ります。

**DefaultPlayables**
https://assetstore.unity.com/packages/essentials/default-playables-95266?locale=ja-JP

まず、DefaultPlayablesでCustomTrackのひな型を作ります。
UnityEditorのWindow>Timeline Playable Wizardを次のように実行すると CustomTrackのコード一式(Track, Behaviour, Clip, Mixer,Editor/Drawer)が作成されます。
![DefaultPlayable.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298907/4d9846bc-c0f1-9712-138c-4da6cfc95294.jpeg)

###◆ CustomTrackのコードの説明

**Track**
ムービー編集ソフトやシーケンスソフトのトラックだと思えばよいです。
Trackごとに制御したいオブジェクトを１つバインドできます。

**Clip**
トラックのイベントのようなものです。イベントと異なるのは設定されたフレーム(時間)の間だけ
Timeline処理の対象となるところです。ここにTimelineEditorから編集する変数を定義します。

**Behaviour**
Clipのふるまいを定義します。

**BehaviourMixer**
ClipやBehaviourの処理やブレンド処理を行います。ここに書いた処理がTimeline(PlayableDirector)で実行されます。名前が長いのでMixerとリネームすることが多いです。

**Drawer**
なくても大丈夫です。ClipをInspecterで確認したときの見た目の拡張を行います。

## OrbitalTransposerのCustomTrackを作る

Timeline Playable Wizardを実行すると CustomTrackのひな型が作成できました。
・OrbitalTransposerTrack
・OrbitalTransposerClip
・OrbitalTransposerBehaviour
・OrbitalTransposerBehaviourMixer

Timelineに慣れていないときはCustomTrackをどこから作るのか迷うと思いますが
最初にTrackとClipの仕様から考えていくとよいです。

**Trackに何をバインドするか？**
今回CustomTrackで制御したいのは CinemachineVirtualCameraの OrbitalTransposerなので、
OrbitalTransposerTrackにはCinemachineVirtualCameraをバインドすることにします。

**Clipで何を編集したいか？**
OrbitalTransposerClipには OrbitalTransposerで制御したい変数を指定します。
今回はBodyのXAxisにします。他に制御したい変数があれば足していってください。

###◆ OrbitalTransposerTrack

Timeline Playable Wizardで CustomTrackのひな型を生成した場合は Trackに CreateTrackMixerが生成されていると思います。

```public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)```
これはBehaviourMixerを生成するためのMethodですが、標準状態ですと不便なので MixerへTrackBinding(Trackにバインドしたオブジェクト)を渡すように改修するのをオススメします。

```OrbitalTransposerTrack.cs
[TrackColor(0.855f, 0.8623f, 0.87f)]
[TrackClipType(typeof(OrbitalTransposerClip))]
[TrackBindingType(typeof(CinemachineVirtualCamera))]
public class OrbitalTransposerTrack : TrackAsset
{
    public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
    {
        var mixer = ScriptPlayable<OrbitalTransposerMixerBehaviour>.Create(graph, inputCount);
        var trackBinding = go.GetComponent<PlayableDirector>().GetGenericBinding(this) as CinemachineVirtualCamera;

        // MixerにTrackBindingを渡します
        mixer.GetBehaviour().TrackBinding = trackBinding;
        return mixer;
    }

    // Please note this assumes only one component of type CinemachineVirtualCamera on the same gameobject.
    public override void GatherProperties (PlayableDirector director, IPropertyCollector driver)
    {
#if UNITY_EDITOR
        CinemachineVirtualCamera trackBinding = director.GetGenericBinding(this) as CinemachineVirtualCamera;
        if (trackBinding == null)
            return;
#endif
        base.GatherProperties (director, driver);
    }
}
```

### ◆ OrbitalTransposerClip

今回はVirtualCameraが対象物の周りをぐるぐる回るようにするので、OrbitalTransposerの(Bodyの)XAxisを変数として保持します。あと回転する速度も編集したいので、rotationSpeedも変数として持ちます。

今回はClip側に変数を処理するためのEvaluate()も作りました。これはBehaviourに書いてもいいですし、Mixerに持って行ってもよいです。※意味合い的にはBehaviourに書くべきでしょうけど。
引数は weightと(各Clipごとの)normalizeした時間をもらうようにします。

Setup()のOrbitalTransposerBehaviourの生成時では OrbitalTransposerClipを渡すようにしました。
こうすることで、OrbitalTransposerBehaviour経由でMixerはClipの参照が取れるようになります。

```OrbitalTransposerClip.cs
[Serializable]
public class OrbitalTransposerClip : PlayableAsset, ITimelineClipAsset
{
    [SerializeField] float xAxis = 0;
    [SerializeField] float rotationSpeed = 100f;

    OrbitalTransposerBehaviour template;
    OrbitalTransposerValues value;

    /// <summary>
    /// OrbitalTransposerで使う値 (必要なパラメータを追加していきましょう)
    /// </summary>
    public class OrbitalTransposerValues
    {
        /// <summary>
        /// Body/XAxis
        /// </summary>
        public float XAxis { get; set; }
    }

    /// <summary>
    /// セットアップ
    /// </summary>
    public void Setup()
    {
        value = new OrbitalTransposerValues();
        template = new OrbitalTransposerBehaviour(this);
    }

    /// <summary>
    /// 評価値(計算結果)を得る
    /// </summary>
    /// <param name="weight">ウエイト</param>
    /// <param name="normalizedTime">正規化した時間</param>
    /// <returns>評価値</returns>
    public OrbitalTransposerValues Evaluate(float weight, float normalizedTime)
    {
        // VirtualCamera OrbitalTransposerの値の更新
        value.XAxis = xAxis + rotationSpeed * weight * normalizedTime;
        return value;
    }

    public ClipCaps clipCaps
    {
        get { return ClipCaps.Blending; }
    }

    public override Playable CreatePlayable (PlayableGraph graph, GameObject owner)
    {
        Setup();
        var playable = ScriptPlayable<OrbitalTransposerBehaviour>.Create (graph, template);
        return playable;
    }
}
```

### ◆ OrbitalTransposerBehaviour

Timeline Playable WizardでCustomTrackのひな型を生成した場合はBehaviourには何も記述されていません。自分はコンストラクタを追加してClipを保持するようにしました。
上記にも書きましたが、MixerからOrbitalTransposerBehaviourの参照が取れるからです。

```OrbitalTransposerBehaviour.cs
[Serializable]
public class OrbitalTransposerBehaviour : PlayableBehaviour
{
    /// <summary>
    /// 操作対象となるClip
    /// </summary>
    public OrbitalTransposerClip Clip { get; private set; }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public OrbitalTransposerBehaviour()
    {
    }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="clip">対になるClip</param>
    public OrbitalTransposerBehaviour(OrbitalTransposerClip clip)
    {
        Clip = clip;
    }
}
```

###◆ OrbitalTransposerBehaviourMixer

```public override void ProcessFrame(Playable playable, FrameData info, object playerData)```
にフレーム(時間)ごとのTimelineの処理を書きます。ここでTrackに設定した全Clipの値を更新します。

ProcessFrame()では、だいたい次の処理をすることになると思います。

1) 各ClipごとのWeightを取得 (Weightはブレンドありのときに使います)
2) 各Clipの参照を得るためにBehaviourを取得
3) 各Clipを処理するためにClipごとのNormalizeした時間を計算
4) 各Clipの値を更新
5) Trackにバインドしたオブジェクトへ結果を反映

**■ 注意**
<font color="Red">Timelineはシーク処理やClipのブレンド処理をするため、フレームごとに全Clipを走査しています。もしTrackにぶら下がっているClip数が非常に多い場合は ProcessFrameの処理を見直したほうがよいです。</font>

```OrbitalTransposerBehaviourMixer.cs
public class OrbitalTransposerMixerBehaviour : PlayableBehaviour
{
    CinemachineVirtualCamera m_TrackBinding = null;
    CinemachineOrbitalTransposer orbital = null;
    CinemachineComposer composer = null;

    public CinemachineVirtualCamera TrackBinding
    {
        get => m_TrackBinding;
        set => m_TrackBinding = value;
    }

    public override void OnGraphStart(Playable playable)
    {
        if (!m_TrackBinding)
        {
            Debug.LogError("OrbitalTransposerTrackにVirtualCameraがバインドされてません");
            return;
        }
    }

    void PreProcess(CinemachineVirtualCamera component)
    {
        if (orbital == null)
        {
            orbital = component.GetCinemachineComponent<CinemachineOrbitalTransposer>();
        }

        if (composer == null)
        {
            composer = component.GetCinemachineComponent<CinemachineComposer>();
        }
    }

    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        m_TrackBinding = playerData as CinemachineVirtualCamera;
        if (m_TrackBinding == null)
            return;

        PreProcess(m_TrackBinding);

        int inputCount = playable.GetInputCount ();     
        float totalWeight = 0f;
        // float greatestWeight = 0f;

        // 各TimelineClipの値を更新
        for (int i = 0; i < inputCount; i++)
        {
            float inputWeight = playable.GetInputWeight(i);
            ScriptPlayable<OrbitalTransposerBehaviour> inputPlayable = (ScriptPlayable<OrbitalTransposerBehaviour>)playable.GetInput(i);
            OrbitalTransposerBehaviour input = inputPlayable.GetBehaviour ();

            // Normalizeした時間を計算してTimelineClipの値を計算
            var normalizedTime = (float)inputPlayable.GetTime() / (float)inputPlayable.GetDuration();

            // 評価
            var values = input.Clip.Evaluate(inputWeight, normalizedTime);

            // VirtualCameraのOrbitalTransposerの値を更新
            orbital.m_XAxis.Value = values.XAxis;

            totalWeight += inputWeight; // 使わないなら不要
        }
    }
}
```

## 完成

TrackにバインドしたVirtualCameraが対象物の周りをぐるぐる回るようなCustomTrackができました。Cinemachineに限らずこんな感じにTimelineで制御可能です。

あと、今回のOrbitalTransposerTrackを使うときに、Trackに指定したVirtualCameraをCinemachineTrackで指定するのを忘れないでください。**CinemachineTrackは指定したVirtualCameraへ切り替えるTrackなので CinemachineをTimelineで制御するときは必ず使います。**

![OrbitalTransposerTrack.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298907/53bb48b3-dc66-6e0d-583e-046650b4cf3c.gif)

## もう少し使いやすくする
CustomTrackを作成したら、利用者が編集しやすくなるようにさらなる改修を考えてみましょう。例えば、今回は次のようにするとより編集しやすくなるかもしれません。

* 回転をSpeedで更新していますが、編集側を考えて回転数を入力するように改修する。
* 回転具合をAnimationCurveで調整できるように改修する。

以上になります。

今回はCinemachineをTimelineで制御する方法とCustomTrackを作る際のポイントについて説明しました。皆さんのアイデアで素敵なCustomTrackを作成してみてください。