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

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

# はじめに

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

## ターゲット

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

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


## 得られるもの


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

この`ClipEditor`と同様に[トラックに対する見た目の拡張に関する記事](https://qiita.com/jukey17/items/110534b3e13a57c57a89)も書いていますので合わせて参照ください

# 本題

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

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

## `ClipEditor`

まず初めにコードの全容を一気に公開し、その後で1つずつ掘り下げていく形で各機能を紹介します

```csharp
using System.Collections.Generic;
using System.Linq;
using UnityEditor.Timeline;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

[CustomTimelineEditor(typeof(SampleClip))]
public class SampleClipEditor : ClipEditor
{
    public override ClipDrawOptions GetClipOptions(TimelineClip clip)
    {
        return new ClipDrawOptions
        {
            errorText = GetErrorText(clip),
            highlightColor = GetDefaultHighlightColor(clip),
            icons = Enumerable.Empty<Texture2D>(),
            tooltip = "Tooltip"
        };
    }

    public override void DrawBackground(TimelineClip clip, ClipBackgroundRegion region)
    {
        base.DrawBackground(clip, region);
    }

    public override void OnCreate(TimelineClip clip, TrackAsset track, TimelineClip clonedFrom)
    {
        base.OnCreate(clip, track, clonedFrom);
    }

    public override void OnClipChanged(TimelineClip clip)
    {
        base.OnClipChanged(clip);
    }

    public override void GetSubTimelines(TimelineClip clip, PlayableDirector director, List<PlayableDirector> subTimelines)
    {
        base.GetSubTimelines(clip, director, subTimelines);
    }
}
```

### `CustomTimelineEditorAttribute`

```csharp
[CustomTimelineEditor(typeof(SampleClip))]
public class SampleClipEditor : ClipEditor
```

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

### `ClipDrawOptions GetClipOptions(TimelineClip clip)`

```csharp
/// <summary>
/// クリップの描画を拡張するためのオプションデータを返すメソッド
/// </summary>
/// <param name="clip">対象となるクリップの情報</param>
/// <returns>ClipDrawOptions</returns>
public override ClipDrawOptions GetClipOptions(TimelineClip clip)
```

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

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

トラックの方ではこのDrawOptions構造体のみでしか見た目の拡張はできませんでしたが、クリッでは背景の拡張も可能です
※後述で解説

### `ClipDrawOptions`

```csharp
return new ClipDrawOptions
{
    // エラー表示に使うテキスト
    errorText = GetErrorText(clip),
    // クリップの色
    highlightColor = GetDefaultHighlightColor(clip),
    // アイコン画像
    icons = Enumerable.Empty<Texture2D>(),
    // ツールチップ表示
    tooltip = "Tooltip"
};
```

クリップに対してどのような見た目の変更ができるのかをまとめているのが`ClipDrawOptions`です

#### `string ClipDrawOption.errorText`

エラー表示に利用するテキストを設定します
テキストを設定するとアイコンがワーニング時のものになり、アイコンにマウスオーバーすると設定したテキストが表示されます

注意しなければならないのは**自分でエラーかどうかを判断してエラーだった場合にこのプロパティにテキストを設定する**という仕様だということです
エラーがない(正常動作をしている)場合は`string.Empty`(つまり空文字)を入れます

基底クラスの`ClipEditor`には**エラーかどうかを判断した上でテキストを返す`GetErrorText()`**というメソッドが用意されています

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

`Object TimelineClip.asset` プロパティに対応する`PlayableAsset`(今回の例で言うと`SampleClip`)の参照が取れるので、そこから更に中身を見てエラー判定をしましょう

#### `Color ClipDrawOption.highlightColor`

クリップの色を指定することができます

`Color GetDefaultHighlightColor(TimelineClip clip)`メソッドを利用するとトラック側に設定されている色を取得することができます

しかし2019/12/22現在で利用できるVer1.2.9まででは、ここで取得できるトラックの色は`TrackColorAttribute`で設定した色のみしか取れません
**`TrackEditor`で設定した`Color TrackDrawOptions.trackColor`の値は取得できない**ため注意してください

※`TrackEditor CustomTimelineEditorCache.GetTrackEditor(TrackAsset track)`というメソッドからそのトラックに対応した`TrackEditor`を取得できるようになっていますが、`CustomTimelineEditorCache`クラスがpublicではないため、利用するにはReflectionの機能を使う必要があります

#### `IEnumerable<Texture2D> ClipDrawOption.icons`

クリップのアイコン表示を設定できます

ここで設定したアイコンはTimelineWindowの各クリップの名前が表示される部分の右隣に表示されます
トラックとは違い**任意の個数を設定できます(横に並ぶ形で表示される)**

しかしここで設定した内容はインスペクター側のアイコン表示には使われていないようです(
なぜなのか
)

#### `string ClipDrawOption.tooltip`

クリップのツールチップ表示を設定できます

### `void DrawBackground(TimelineClip clip, ClipBackgroundRegion region)`

```csharp
/// <summary>
/// クリップの背景を描画するタイミングで呼ばれるメソッド
/// </summary>
/// <param name="clip">対象となるクリップ</param>
/// <param name="region">クリップの背景を描画するために必要な情報</param>
public override void DrawBackground(TimelineClip clip, ClipBackgroundRegion region)
```

対象となるクリップの背景の描画を丸ごとカスタマイズすることができます

**`AudioTrack`ではこの機能を使ってクリップの背景に波形を表示する**拡張を行っています
ただし、この部分はエディタ拡張をする際に利用する`OnGUI`イベントとほぼ同等の頻度で呼ばれるものなので、パフォーマンスに気を配る必要があります
`AudioTrack`で使われる`AudioPlayableAssetEditor`クラスでも`TimelineClip`ごとに`WaveformPreview`のインスタンスをキャッシュしていますので参考にしてみると良いでしょう


### `void OnCreate(TimelineClip clip, TrackAsset track, TimelineClip clonedFrom)`

```csharp
/// <summary>
/// クリップが新しく生成されたタイミングで呼ばれるメソッド
/// </summary>
/// <param name="clip">生成されたクリップ</param>
/// <param name="track">クリップの親となるトラック</param>
/// <param name="clonedFrom">複製元のクリップ</param>
public override void OnCreate(TimelineClip clip, TrackAsset track, TimelineClip clonedFrom)
```

対象となるクリップが生成されたタイミングを取得することができます

`GetClipOptions()`や`DrawBackground()`がかなりの頻度で呼び出されるので生成時に1度だけ処理すれば良いものはここにまとめてキャッシュするような作りにしましょう


### `void OnClipChanged(TimelineClip clip)`

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

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

以前に[若干強引な方法でクリップの内容に合わせて表示名を変更する記事](https://qiita.com/jukey17/items/1e6c5b4b0c9f63399e14)を書きましたが、このタイミングを使えば責務的にもスッキリとした実装にできそうです


### `void GetSubTimelines(TimelineClip clip, PlayableDirector director, List<PlayableDirector> subTimelines)`

```csharp
/// <summary>
/// クリップの内容に紐付いて子のTimelineがある場合にそれをTimelineWindowに伝えるためのメソッド
/// </summary>
/// <param name="clip">対象となるクリップ</param>
/// <param name="director">クリップを再生しているPlayableDirector</param>
/// <param name="subTimelines">子Timelineの追加先List</param>
public override void GetSubTimelines(TimelineClip clip, PlayableDirector director, List<PlayableDirector> subTimelines)
```
このメソッドは他のものとは違って少し特殊です

既存の実装では**`ControlTrack`の実装に使われています**

`ControlTrack`はクリップ単位でシーン上のオブジェクトやPrefabを設定しそれを操作できるトラックですが、オプションとして`PlayableDirector`に時間を伝播できる仕様になっています
この際に、`PlayableDirector`がアタッチされた`GameObject`が設定されているクリップは、そのクリップをダブルクリックすることでその`PlayableDirector`に設定されている`TimelineAsset`を**今開いている`TimelineAsset`の子Timelineとして開くことができます**

第一引数の`TimelineClip`に関連する`TimelineAsset`がセットされた`PlayableDirector`の参照を第三引数の`List<PlayableDirector> subTimelines`にAddすることでTimelineウィンドウの機能として対象のクリップからその子Timelineへジャンプすることできるようになります

複数の`PlayableDirector`をAddした場合、恐らく最初？にAddした`PlayableDirector`の`TimelineAsset`が子TimelineとしてTimelineウィンドウとして表示されます
その他のAddした`PlayableDirector`を表示したい場合は右クリックメニューから`Edit Sub-Timelines`の項目を選ぶことで任意の`PlayableDirector`を選ぶ形で表示します

あまり使わない内容ではあると思いますが、`ControlTrack`で使われる`ControlPlayableAssetEditor`クラスに実装内容がありますので気になる方はチェックしてみましょう

# おわりに

今回は`ClipEditor`でどんなことができるのかを深堀りしてみました

[トラックの拡張](https://qiita.com/jukey17/items/110534b3e13a57c57a89)とは違い、背景の描画について拡張できたりするのが特徴です

上記記事と合わせて、皆さんが作る独自のTimeline編集環境が少しでもよいものになると幸いです