[Unity][Timeline] Unity2019.2から使えるようになったTrackEditorでトラックの見た目を拡張する

この記事は[Unity #2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/unity2)の12/11(水)の記事です

# はじめに

この記事ではUnityのTimeline機能に存在する`TrackEditor`について解説します

## ターゲット

Unityを使った開発を行っており**Timeline機能を拡張して実装を進めているエンジニア**がメインターゲットです
Unityの基礎的な内容やTimeline、Editor拡張などで頻出する単語・機能などに関しての詳細な説明は省略しますので予めご了承ください

また、この`TrackEditor`の機能はUnity2019.2から利用できる**Timeline Versition 1.1.*以降**から使えるようになるものです
※詳しくは[changelog](https://docs.unity3d.com/Packages/com.unity.timeline@1.2/changelog/CHANGELOG.html)を参照


## 得られるもの


`UnityEditor.Timeline.TrackEditor`にどのような機能があるのかを知ることができます
これらは、Timelineのウィンドウに表示されるトラック部分の見た目を拡張するための機能なため、使い方を知ることで**Timeline周りのツール開発で見た目の拡張をより広げることができます**

この機能に合わせてクリップの見た目を拡張する`ClipEditor`の機能も追加されていますが、そちらに関しては[この記事](https://qiita.com/jukey17/items/e4db4028aeb650819bc5)にて紹介しています

# 本題

下記のような独自で定義したTrackを例に`TrackEditor`の実装を紹介していきます

```csharp:SampleTrack.cs
[TrackClipType(typeof(SampleClip)]
public class SampleTrack : TrackAsset
{
    // 省略...
}
```
```csharp:SampleClip.cs
public class SampleClip : PlayableAsset, ITimelineClipAsset
{
    // 省略...
}
```

## `TrackEditor`

いきなり全コードを貼り付けていきますが、1つずつ見ていきましょう

```csharp
/// <summary>
/// SampleTrackのEditor拡張
/// </summary>
[CustomTimelineEditor(typeof(SampleTrack))]
public class SampleTrackEditor : TrackEditor
{
    /// <summary>
    /// トラックの見た目を拡張するオプションデータを返すためのメソッド
    /// </summary>
    /// <param name="track">対象となるトラック</param>
    /// <param name="binding">Bindされているオブジェクト</param>
    /// <returns>TrackDrawOptions</returns>
    public override TrackDrawOptions GetTrackOptions(TrackAsset track, Object binding)
    {
        Debug.Log($"SampleTrackEditor.GetTrackOptions: track={track}, binding={binding}");
        return new TrackDrawOptions
        {
            errorText = base.GetErrorText(track, binding, TrackBindingErrors.All),
            trackColor = base.GetTrackColor(track),
            minimumHeight = TrackEditor.DefaultTrackHeight,
            icon = null,
        };
    }

    /// <summary>
    /// トラックが新しく生成されたタイミングで呼ばれるメソッド
    /// </summary>
    /// <param name="track">生成されたトラック</param>
    /// <param name="copiedFrom">コピー＆ペーストで生成された場合のコピー元</param>
    public override void OnCreate(TrackAsset track, TrackAsset copiedFrom)
    {
        Debug.Log($"SampleTrackEditor.OnCreate: track={track}, copiedFrom={copiedFrom}");
        base.OnCreate(track, copiedFrom);
    }

    /// <summary>
    /// トラックの内容に変更があったタイミングで呼ばれるメソッド
    /// </summary>
    /// <param name="track">変更があったトラック</param>
    public override void OnTrackChanged(TrackAsset track)
    {
        Debug.Log($"SampleTrackEditor.OnTrackChanged: track={track}");
        base.OnTrackChanged(track);
    }
}
```

### `CustomTimelineEditorAttribute`

```csharp
[CustomTimelineEditor(typeof(SampleTrack))]
public class SampleTrackEditor : TrackEditor
```

`SampleTrackEditor`を`SampleTrack`の`TrackEditor`として認識させるためにクラスの宣言に対して`CustomTimelineEditorAttribute`を付ける必要があります

### `TrackDrawOptions TrackEditor.GetTrackOptions(TrackAsset track, Object binding)`

```csharp
/// <summary>
/// トラックの見た目を拡張するオプションデータを返すためのメソッド
/// </summary>
/// <param name="track">対象となるトラック</param>
/// <param name="binding">Bindされているオブジェクト</param>
/// <returns>TrackDrawOptions</returns>
public override TrackDrawOptions GetTrackOptions(TrackAsset track, Object binding)
```

トラックの見た目を拡張するための構造体を生成してこのメソッドで返却することでそのトラックの見た目を変更することができます

引数から分かるようにトラック単位で呼び出されるので**同じトラックの型であっても内容によって見た目を変更するといったことが可能**です

現状だと変更できる見た目は`TrackDrawOptions`の内容に限定されてしまいます
※後述で解説

#### bindオブジェクト

引数の`binding`にはそのトラックにバインドされているオブジェクトが入ってきます
今回は`SampleTrack`のクラス宣言時に`TrackBindingTypeAttribute`を付けていないため、トラックにオブジェクトをバインドしない実装になっています
この場合は`binding`は常に`null`が入ってきます

また、この**バインドされているオブジェクトの型を取得するための`GetBindingType()`**というメソッドが用意されています
bindingに値が入っているかに関係なく`TrackAsset`だけで掘り起こしてくれるので、型情報が必要な場合はこちらを利用しましょう

#### 呼ばれる頻度と処理負荷

トラックの再描画命令が飛ぶたびに呼ばれます
マウスオーバーするだけでも呼ばれたりするので**かなりの頻度**で呼び出されます
更にそのトラックの数だけ呼び出されるので、ここでの処理はあまり複雑なものにしてしまうと**あっという間に重くなります**

処理結果をキャッシュしたりして**毎回負荷の高い処理が走らないようにする工夫が必要**です

### `TrackDrawOptions`

```csharp
return new TrackDrawOptions
{
    // エラー表示に使うテキスト
    errorText = base.GetErrorText(track, binding, TrackBindingErrors.All),
    // トラックの色
    trackColor = base.GetTrackColor(track),
    // トラックの最小の高さ
    minimumHeight = TrackEditor.DefaultTrackHeight,
    // アイコン画像
    icon = null,
};
```

トラックに対してどのような見た目の変更ができるのかをまとめているのが`TrackDrawOptions`構造体です

#### `string TrackDrawOptions.errorText`

エラー表示に利用するテキストを設定します
テキストを設定するとアイコンがワーニング時のものになり、アイコンにマウスオーバーすると設定したテキストが表示されます
![スクリーンショット 2019-12-11 1.52.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146374/a91a5dbe-38b2-5d77-1fd4-70c9925615ed.png)
注意しなければならないのは**自分でエラーかどうかを判断してエラーだった場合にこのプロパティにテキストを設定する**という仕様だということです
エラーがない(正常動作をしている)場合は`string.Empty`(つまり空文字)を入れます

基底クラスの`TrackEditor`には**エラーかどうかを判断した上でテキストを返す`GetErrorText()`**というメソッドが用意されています
また、このメソッドでは`TrackBindingErrors`というビットフラグを使ってどのような内容をエラーとして含めるかを決めることもできます

ビット演算を使っているのになぜか`FlagsAttribute`が付いていない


基本的にはこのメソッドで標準のエラー判定をしつつ、更に自前でエラー判定が必要であれば追加で判定をしていくという実装をすることになるでしょう

#### `Color TrackDrawOptions.trackColor`

トラックの色を指定することができます
![スクリーンショット 2019-12-11 2.10.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146374/ca589936-0e08-b413-4088-00ad80d18fb1.png)
この処理内容は、トラック側のクラス宣言時に`TrackColorAttribute`で設定するアプローチと同様の内容です
Attributeによる指定ではトラックの型に対して1種類の色しか指定できませんでしたが、**この方法では同じトラックの型でも色を使い分けることができます**

もしどちらにも色の指定があった場合はこの**`TrackDrawOptions`で設定した色が優先**されます

また、`TrackColorAttribute`で設定されている色は`GetTrackColor()` で取得することができます

#### `float TrackDrawOptions.minimumHeight`

トラックの最小の高さを設定することができます
![スクリーンショット 2019-12-11 2.23.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146374/009c81ee-95f0-2c5f-6d44-36f9567aa548.png)
`ClipEditor`による拡張でクリップの見た目を作る際に、デフォルトの高さでは表現し切れないケースでもここで高さを変更することで対応することが可能です

Timelineのウィンドウでもトラックの高さを変更することが可能ですが、**ここで指定した高さよりも小さくならないようになります**

#### `Texture2D TrackDrawOptions.icon`

トラックのアイコン表示を設定できます
ここで設定したアイコンはTimelineウィンドウの各トラックの名前が表示される部分とトラックを選択した際に表示されるそのトラックのインスペクター表示部分のヘッダーに使用されます
![スクリーンショット 2019-12-11 2.42.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146374/11ce54eb-7f24-addc-b80b-17294cf59d27.png)

キャラクター単位で同じようなトラックを使うような場合、キャラクターごとにアイコン画像を用意するなどして**同一種別のトラックでも設定している内容を視覚的に間違えにくい状態を作れそう**です

### `void OnCreate(TrackAsset track, TrackAsset copiedFrom)`

```csharp
/// <summary>
/// トラックが新しく生成されたタイミングで呼ばれるメソッド
/// </summary>
/// <param name="track">生成されたトラック</param>
/// <param name="copiedFrom">コピー＆ペーストで生成された場合のコピー元</param>
public override void OnCreate(TrackAsset track, TrackAsset copiedFrom)
```

対応するトラックが生成されたタイミングを取得することができます

前述の`GetTrackOptions()`メソッドがかなりの頻度で呼び出されるため、トラックの生成時に1度処理すれば十分なものはここで処理してしまい、`GetTrackOptions()`メソッドでは既に処理されたその内容を使い回すような実装にするとパフォーマンスを稼げるでしょう

ただし、 この**`TrackEditor`は作られたトラックごとにインスタンスが存在するわけではない**ので値のキャッシングには注意が必要です ※後述で解説
引数に渡されてくる`TrackAsset`の情報をキーにして保持しておくのがよいでしょう

#### 複製されたトラック

2番目の引数`copiedFrom`にはコピー元のトラックが渡されてきます
ここは新規でトラックが生成された場合は`null`が入ってくるので、ここを見ることで今回生成されたトラックが**新規作成されたものなのか複製されたものなのかを判断することができます**

Unity標準の`ActivateTrack`では、ここを使って**新規トラックの作成の場合は一緒にクリップも生成する**という挙動を作っています
参考にできそうです

### `void OnTrackChanged(TrackAsset track)`

```csharp
/// <summary>
/// トラックの内容に変更があったタイミングで呼ばれるメソッド
/// </summary>
/// <param name="track">変更があったトラック</param>
public override void OnTrackChanged(TrackAsset track)
```

トラックの内容が変更されたかどうかを検知することができます

ただし2019.3.0f1で動作確認を取ったところ、**関係するところに少しでも変更が入ると何でも通知が飛んできてしまう**ようでちょっと使いこなすのが大変そうな雰囲気でした

どういう変更だったかのイベント種別みたいなのも引数で欲しいな


試してみたのは以下の内容です

#### トラックの名前を変更する

こちらは名前を変更したトラックが引数に入ってくる形で1度だけ呼び出されました

#### トラック・クリップの追加・移動・削除

- トラックを追加する(`SampleTrack`)
- トラックを追加する(`SampleTrack以外`)
- トラックを移動する(`SampleTrack`)
- トラックを移動する(`SampleTrack以外`)
- トラックを削除する(`SampleTrack`)
- トラックを削除する(`SampleTrack以外`)
- クリップを追加する(`SampleClip`)
- クリップを追加する(`SampleClip以外`)
- クリップを移動する(`SampleClip`)
- クリップを移動する(`SampleClip以外`)
- クリップを削除する(`SampleClip`)
- クリップを削除する(`SampleClip以外`)

これらの動作は同一Timeline内に存在する`SampleTrack`の数だけ呼び出されました
※要するに全てに変更があった扱い

#### 親Timelineから子Timelineへ移動

`ControlTrack`でTimelineを階層構造にしている際に親Timelineから子Timelineへの移動を行った場合の挙動です
こちらは子Timelineに存在する`SampleTrack`のみ呼び出されました
(ただし`SampleTrack`の数だけ呼び出される)

#### 子Timelineから親Timelineへ移動

上記の状態から親Timelineを再度ウィンドウに表示させる場合の挙動です
こちら逆に親Timelineに存在する`SampleTrack`のみ呼び出されました
(ただし`SampleTrack`の数だけ呼び出される)

#### `SampleTrackEditor`のインスタンス

また、`OnTrackChanged()`の挙動を調べる際に`SampleTrackEditor`のインスタンスがどのように扱われているかも整理できました

同一Timeline内では1つの`SampleTrackEditor`のインスタンスが使い回され、呼び出されるメソッドの引数に各`SampleTrack`のインスタンスが入ってくるという挙動をしていました

しかし、子Timelineと組み合わせた場合は親Timelineと子Timelineで`SampleTrackEditor`のインスタンスが違っていました

どうやら**`TimelineAsset`（もしくはTimelineAssetのView）ごとに`SampleTrackEditor`のインスタンスが生成されている**ようでした

これらの挙動は実際に拡張する上でキャッシング処理を作る際に意識しておいたほうが良いでしょう

# おわりに

今回は`TrackEditor`でどんなことができるのか深堀りしてみました
まだまだやれることは限られていますが、少しずつTimelineウィンドウ上もカスタマイズできるようになってきています

Timeline機能はカットシーンなどのアセット作成をする上で非常に強力なツールですので、**見た目もカスタマイズしてプロジェクト仕様に合ったツール開発がドンドンできるようになっていけばいいなと思っています**

`TrackEditor`だけでもそこそこなボリュームになってしまったので、また時間を作って今度は`ClipEditor`の方も掘り下げて行ければ良いなと考えています


Unityさん、もっとカスタマイズできるようにして〜＞＜