UnityのTimelineTrackを実装する

Unity2017.1から登場したPlayableAPIとTimelineについて。

* 2018/05/15 サンプル書き直し
* 2018/04/25 ちょっと整理
* 2017/12/25 修正。1ファイルではタイムラインをロードできない(トラック消滅・・・)ことが発覚。２ファイルに分けた

![customtrack.jpg](https://qiita-image-store.s3.amazonaws.com/0/59664/d8e7ac1f-6069-b097-5f07-be2f21ff34a4.jpeg)

例としてクリップがAlpha値を保持し、TrackのBindingにCanvasGroupを保持するものを実装する。

* [Timeline (Unity 2017b5) Fade sample](https://gist.github.com/tsubaki/841bf990affab423b80ff4c5949670f9)
* [DefaultPlayable](https://assetstore.unity.com/packages/essentials/default-playables-95266)

を参考にして作った。
参考元はフェードアウト・インという変化するところをクリップ化しているけど、作ったのはクリップにアルファ値を設定する。

* https://github.com/ousttrue/UnityTimelineTracks

Unity2017.4で動作確認。

## 名前の付け方

* AlphaClipと表示するためにAlphaと名付ける必要がある
* 何のアルファかわからないのでバインドターゲットと対象の値を連結してCanvasGroupAlphaとすることにした。

![add_clip.jpg](https://qiita-image-store.s3.amazonaws.com/0/59664/ce9d8818-d813-52db-3fab-ba6c98161032.jpeg)

* 後ろにClipが追加されるのでAlphaClipと名付けるとAlphaClipClipになってしまう
* トラックはCanvasGroupAlphaTrackとした
* トラックはCanvasGroupAlphaTrackTrackにはならない

## 最小限
**TrackとClipでファイル名とクラス名の一致が必要**だった

* Clipでファイル名とクラス名を不一致にするとClipを作ることができない
* Trackでファイル名とクラス名を不一致にするとUnityを落として次回Timelineをロードするときにトラックが消滅する・・・

``CanvasGroupAlpha.cs``

```csharp
using System;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;


namespace UnityTimelineTracks
{

    [Serializable]
    public class CanvasGroupAlphaBehaviour : PlayableBehaviour
    {
        [SerializeField, Range(0, 1)]
        public float Alpha; // Clipのインスペクタで表示される
    }


    public class CanvasGroupAlphaMixerBehaviour : PlayableBehaviour
    {
        CanvasGroup m_trackBinding;
        float m_initialValue;

        public override void OnGraphStop(Playable playable)
        {
            if (m_trackBinding != null)
            {
                // 初期値の復旧
                m_trackBinding.alpha = m_initialValue;
                m_trackBinding = null;
            }
        }

        public override void ProcessFrame(Playable playable, FrameData info, object playerData)
        {
            var trackBinding = playerData as CanvasGroup;
            if (trackBinding == null)
            {
                return;
            }

            if (m_trackBinding == null)
            {
                m_trackBinding = trackBinding;
                // 初期値の記憶
                m_initialValue = m_trackBinding.alpha;
                return;
            }

            // 全clipのalpha値をweightに応じて合計する
            int inputCount = playable.GetInputCount();
            float alpha = 0;
            for (int i = 0; i < inputCount; ++i)
            {
                var inputWeight = playable.GetInputWeight(i);
                var inputPlayable = (ScriptPlayable<CanvasGroupAlphaBehaviour>)playable.GetInput(i);
                var input = inputPlayable.GetBehaviour();
                alpha += input.Alpha * inputWeight;
            }

            // 反映
            m_trackBinding.alpha = alpha;
        }
    }


    [Serializable]
    public class CanvasGroupAlpha : PlayableAsset, ITimelineClipAsset
    {
        public ClipCaps clipCaps
        {
            get { return ClipCaps.Blending; }
        }

        [SerializeField]
        public CanvasGroupAlphaBehaviour Template; // CanvasGroupAlphaBehaviourに[Serializable]をつけてインスペクタに出るようにした

        public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
        {
            return ScriptPlayable<CanvasGroupAlphaBehaviour>.Create(graph, Template); // こうするとTemplateのメンバをコピーしてScriptPlayable<CanvasGroupAlphaBehaviour>を生成できる
        }
    }
}
```

CanvasGroupAlphaTrack.cs

```csharp
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;


namespace UnityTimelineTracks
{
    [TrackColor(0, 0, 0)]
    [TrackClipType(typeof(CanvasGroupAlpha))]
    [TrackBindingType(typeof(CanvasGroup))]
    public class CanvasGroupAlphaTrack : TrackAsset
    {
        public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
        {
            // Hack to set the display name of the clip
            foreach (var c in GetClips())
            {
                var clip = (CanvasGroupAlpha)c.asset;
                c.displayName = string.Format("{0:0.00}", clip.Template.Alpha);
            }

            return ScriptPlayable<CanvasGroupAlphaMixerBehaviour>.Create(graph, inputCount);
        }
    }
}
```

PlayableBehaviour, MixerPlayableBehaviour, PlayableAsset(Clip), TrackAssetの４つが必要。
PlayableAsset(Clip)とTrackAssetはScriptableObjectでTimeline.playableアセットの中に保存されるのがポイント。

タイムラインのグラフ開始時に、TrackのアセットからからMixerが生成され、ClipのアセットからPlayableが生成される。
TimelineにおけるMixerの必要性は、後続の``Timelineの作るPlayableGraph``節もご覧ください。

## Blending対応

```csharp

// class CanvasGroupAlpha
    public ClipCaps clipCaps
    {
        get { return ClipCaps.Blending; }
    }
```

クリップがオーバーラップする部分でWeightの合計が1になるようにしてくれる。無いとオーバーラップする部分で両方のWeightが1だった。

![blending.jpg](https://qiita-image-store.s3.amazonaws.com/0/59664/ae1b59ba-f8a0-12c2-b8d8-cd281f8e6e2c.jpeg)

見た目にも斜めになってクロスフェード感。

## Clipの名前を値に応じて見やすくする

Cinemachineにあった。

```csharp

public class AlphaTrack : TrackAsset
{
    public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
    {
        // Hack to set the display name of the clip
        foreach (var c in GetClips())
        {
            var clip = (AlphaClip)c.asset;
            c.displayName = string.Format("{0:0.00}", clip.Alpha);
        }
```

アルファ値をClipの名前にする。

![clipname.jpg](https://qiita-image-store.s3.amazonaws.com/0/59664/2db14a59-cc07-e314-73c3-b204449b697b.jpeg)

見やすくなった。

## Timelineの作るPlayableGraph

Trackを代表するMixerをひとつ作って、Trackに属するすべてのClipを入力に接続する。

![timelinegraph.jpg](https://qiita-image-store.s3.amazonaws.com/0/59664/5c687666-7331-ebef-5a04-6c7e6d515c6c.jpeg)

２つのトラックがあるタイムラインの例。
水色の部分が各トラック、ピンクが各クリップに相当する。

時間進行に応じて各クリップの時間とWeightが変化するので、Mixerで``Weight>0``なクリップの値をトラックにバインドされたオブジェクトに反映する。Weight>0なクリップは、無い、１つ、２つがクロスフェード中の３パターンを考えればよさそう。

## Playableのライフサイクル
タイムラインのPlayableはGraph開始時に生成され終了時に破棄されるため、EditorでPlayせずにタイムラインを操作(preview)する場合のオブジェクトの状態に注意をはらう必要がある。EditorのPreviewでのタイムライン操作は以下の点に注意が必要。

* Play終了時のシーンの変更復帰が起こらないので、Timelineによる変更はずっと残る
* これを防ぐためには、次項の初期値の記憶と復帰が必要
* シーンが開始していないのでMonoBehaviourはAwake, OnEnable, Start等を通過していない。バインド対象のセットアップをPlayable側で実行する必要があるかもしれない。

## 初期値の記憶と復帰
DefaultPlayableのWizardで生成するコードにあった。
この処理を入れておかないと、EditorモードでTimelineを操作した変更がアセット(シーン)の変更として残ってしまう。

```csharp
// mixer playable
        public override void OnGraphStop(Playable playable)
        {
            if (m_trackBinding != null)
            {
                // 初期値の復旧
                m_trackBinding.alpha = m_initialValue;
                m_trackBinding = null;
            }
        }
```
Playモードが終了してタイムラインが停止する時、
EditorモードでTimelineのGUIのフォーカスが外れた時、Previewをオフにした時などにOnGraphStopが呼ばれ値をTimeline操作前に戻す。