[Unity][Timeline] 独自実装のトラック上で生成されるクリップのデフォルトの長さを設定する

## はじめに
※このサンプルは`Unity2018.3.0b4`で動作確認しています
DefaultPlayablesのサンプル実装のようにトラックを独自実装する場合、用途によってはデフォルトで生成されるクリップの長さを変更したい場合があります

しかし、サッと公開されている情報を漁ってみてもどのように設定するのかイマイチ分かりません
ここは内部実装を覗いてみて何か手段がないか探ってみることにしましょう

## 内部実装の解析

早速クリップを生成する内部コードを掘り起こして確認すると、下記のような恐ろしい決め打ち実装がなされていて、そのままでは強制的に5秒でクリップが生成されてしまうような設計になっています

```csharp
public class TimelineClip : ISerializationCallbackReceiver
{
    // ...(省略)
    public static readonly float kDefaultClipDurationInSeconds = 5f;
    // ...(省略)
}
```

```csharp
public abstract class TrackAsset : PlayableAsset, IPropertyPreview, ISerializationCallbackReceiver
{
    // ...(省略)
    internal TimelineClip CreateNewClipContainerInternal()
    {
        // ...(省略)
        timelineClip.duration = (double) TimelineClip.kDefaultClipDurationInSeconds;
        // ...(省略)
        return timelineClip;
    }
    // ...(省略)
}
```

しかし、もう少し内部実装を追ってみるととある項目を見てクリップ生成時の長さの設定を上書きしている箇所を発見することができます

```csharp
public abstract class TrackAsset : PlayableAsset, IPropertyPreview, ISerializationCallbackReceiver
{
    // ...(省略)
    private TimelineClip CreateClipFromAsset(ScriptableObject playableAsset)
    {
        TimelineClip containerInternal = this.CreateNewClipContainerInternal();
        // ...(省略)
        IPlayableAsset playableAsset1 = playableAsset as IPlayableAsset;
        if (playableAsset1 != null)
        {
            // なんかここでIPlayaleAsset.durationを使ってTimelineClip.durationを設定しているぞ(ΦωΦ)！
            double duration = playableAsset1.duration;
            if (!double.IsInfinity(duration) && duration > 0.0)
                containerInternal.duration = Math.Min(Math.Max(duration, TimelineClip.kMinDuration), TimelineClip.kMaxTimeValue);
        }
        // ...(省略)
    }
    // ...(省略)
}
```

## 結論

`IPlayableAsset` はトラックを独自実装する際に`TrackClipTypeAttribute`で設定するクラスが継承することになります
継承関係としては`IPlayableAsset` -> `PlayableAsset` -> `独自PlayableAsset` という形になり、独自実装側で何もしない場合`duration`プロパティは`PlayableAsset`で`double.PositiveInfinity`が定義されることになっています

よってこの`duration`プロパティを独自実装側でさらに`override`してあげて、任意のデフォルト時間を設定してあげれば、クリップが生成されたときの長さを自由に設定できるようになるということになります

最後に、実際にデフォルトのクリップの長さを変更した実装例を紹介して終わりにしたいと思います

## サンプルコード

トラックの独自実装の一連の流れを下記に全て並べてみていますが、重要なのは`SamplePlayableAsset`クラスで`override`している`double duration`プロパティです

```csharp:SamplePlayableAsset.cs
// クリップ単位で持つPlayableAsset
public class SamplePlayableAsset : PlayableAsset, ITimelineClipAsset
{
    const double DefaultDuration = 1.0f;

    [SerializeField] float num;

    public float Num
    {
        get { return num; }
    }

    // IPlayableAsset.durationを実装するとその値をデフォルト値としてTimelineClipを生成してくれる
    public override double duration
    {
        get { return DefaultDuration; }
    }

    public ClipCaps clipCaps
    {
        get { return ClipCaps.Blending; }
    }
    
    // クリップ単位のPlayableを生成する
    public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    {
        // 自身（PlayableAsset）の情報を持たせたBehaviourでScriptPlayableを生成する
        // ※これによりProcessFrame内でPlayableからBehaviourを取り出し、クリップ単位のPlayableAsset情報にアクセスできるようになる
        return ScriptPlayable<SampleBehaviour>.Create(graph, new SampleBehaviour(this));
    }
}
```

```csharp:SampleBehaviour.cs
// クリップ単位の処理する
public class SampleBehaviour : PlayableBehaviour
{
    // ここではミキサー側からPlayableAsset情報を取り出すためのプロパティを定義しているだけになっている
    public SamplePlayableAsset Asset { get; private set; }

    public SampleBehaviour(SamplePlayableAsset asset)
    {
        Asset = asset;
    }

    public SampleBehaviour()
    {
    }
}
```

```csharp:SampleTrack.cs
// 独自実装のトラック
[TrackClipType(typeof(SamplePlayableAsset))]
[TrackBindingType(typeof(SampleComponent))]
public class SampleTrack : TrackAsset
{
    // クリップ全体を処理できるミキサーの生成
    public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
    {
        return ScriptPlayable<SampleMixer>.Create(graph, inputCount);
    }
}
```

```csharp:SampleMixer.cs
// クリップ全体を処理するためのミキサー
public class SampleMixer : PlayableBehaviour
{
    // 現在のフレームの処理を実装する
    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        var component = playerData as SampleComponent;
        if (component == null)
        {
            return;
        }

        // ここらへんはクリップごとにミキサーがブレンド処理をしている風を装っているだけなので深い意味はない
        var num = 0.0f;
        var inputCount = playable.GetInputCount();
        for (var i = 0; i < inputCount; ++i)
        {
            var weight = playable.GetInputWeight(i);
            if (Mathf.Approximately(weight, 0))
            {
                continue;
            }
            
            var input = (ScriptPlayable<SampleBehaviour>) playable.GetInput(i);
            var behaviour = input.GetBehaviour();
            var asset = behaviour.Asset;

            num += (asset.Num * weight);
        }

        component.Num = num;
    }
}
```

```csharp:SampleComponent.cs
// 独自トラックにBindする対象となるコンポーネント
public class SampleComponent : MonoBehaviour
{
    [SerializeField] float num;
    
    // ミキサーから設定される想定のプロパティ
    public float Num
    {
        get { return num; }
        set { num = value; }
    }

    void Update()
    {
        // とりあえｚｙミキサーから設定されたパラメータをデバッグログ出力している
        Debug.LogFormat("num={0}", Num);
    }
}
```