Unity Timeline 1.4で追加されたTimelineActionの使い方

この記事は[Unity #3 Advent Calendar 2020](https://qiita.com/advent-calendar/20２０/unity3)の12/12(土)の記事です。

# はじめに

この記事ではUnityのTimeline機能に存在する`TimelineAction`とそれに関連する機能について解説します。

## ターゲット

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

またここで解説する機能は**Unity Timeline 1.4.0以降**から使えるようになるものです。
詳しくは[changelog](https://docs.unity3d.com/Packages/com.unity.timeline@1.4/changelog/CHANGELOG.html)を参照してください。

## 得られるもの

[`UnityEditor.Timeline.Actions.TimelineAction`](https://docs.unity3d.com/Packages/com.unity.timeline@1.4/api/UnityEditor.Timeline.Actions.TimelineAction.html)の基本的な使い方を知ることができます。

この機能は主に**Timelineウィンドウ上のコンテキストメニューに独自の処理を追加する**ことを可能とします。

1.4.0以前のバージョンではTimelineウィンドウに関わるクラス群がほとんど`internal`な宣言になっていたこともあり、独自の処理をTimelineウィンドウ内に追加することは現実的ではなく、**オリジナルの`EditorWindow`を定義して利用者にはTimelineウィンドウとその独自ウィンドウを一緒に開いてもらって編集をするようなワークフローを組んでいた**方も多いと思います。

今回登場したこの`TimelineAction`の機能によりコンテキストメニューを使えるようになったので、そのような独自ウィンドウをわざわざ作らなくても済むことが大幅に増えました。

# 本題

Timeline機能も昔と比べてサポートが充実してきました。
[公式のTimelineActionに関するAPIドキュメント](https://docs.unity3d.com/Packages/com.unity.timeline@1.4/api/UnityEditor.Timeline.Actions.TimelineAction.html)を見に行くとしっかりとサンプルコードが載っています。
今回はこのサンプルコードを軸に解説を進めます。

```csharp:SampleTimelineAction.cs
[MenuEntry("Custom Actions/Sample Timeline Action")]
public class SampleTimelineAction : TimelineAction
{
    public override ActionValidity Validate(ActionContext context)
    {
        return ActionValidity.Valid;
    }

    public override bool Execute(ActionContext context)
    {
        Debug.Log("Test Action");
        return true;
    }

    [TimelineShortcut("SampleTimelineAction", KeyCode.Q)]
    public static void HandleShortCut(ShortcutArguments args)
    {
        Invoker.InvokeWithSelected<SampleTimelineAction>();
    }
}
```

## `TimelineAction`

`TimelineAction`の機能を使うためには**`TimelineAction`クラスを継承します。**
詳細は後述するそれぞれの項目で解説しますが、このクラスを継承しただけではTimelineウィンドウ上では動作せず、`MenuEntryAttribute`や`TimelineShortcutAttribute`と組み合わせて使用することでTimelineウィンドウ上で処理を実行できるようになります。

### `public override ActionValidity Validate(ActionContext context)`

後述の`Execute`メソッドを実行するかどうかを検証するメソッドです。

#### `ActionValidity`
戻り値の[`ActionValidity`](https://docs.unity3d.com/Packages/com.unity.timeline@1.4/api/UnityEditor.Timeline.Actions.ActionValidity.html)によって動作が変わります。

|ActionValidityの種別|`Execute`メソッドが実行されるかどうか|`MenuEntryAttribute`を使用している場合の挙動|
|:--|:--|:--|
|ActionValidity.Valid|**実行されます。**|コンテキストメニューに選択可能な状態で表示されます。|
|ActionValidity.NotApplicable|実行されません。|**コンテキストメニューには表示されません。**|
|ActionValidity.Inavlid|実行されません。|コンテキストメニューに選択不可能な状態で表示されます。|

特殊なのが`ActionValidity.NotApplicable`で、これはメニューにも表示されなくなります。
使い分けの仕方としては、**条件によって使用可能な処理は`ActionValidity.Invalid`を、そもそもその編集状態では使用することができない処理は`ActionValidity.NotApplicable`**という風にするのが良いと思います。
これは、1つのUnityプロジェクトの中で性質の違う複数のTimelineを編集する必要があるような環境で使えそうです。
(ゲーム開発で例えると、ガチャ演出のTimeline編集専用のActionと奥義演出のTimeline編集専用のAction機能が同居しているなどのケース)

逆に言えば、そこまで規模の大きくない編集環境であれば、基本的には`ActionValidity.Valid`と`ActionValidity.Invalid`の2つを使えばよいと思います。


#### `ActionContext`

このActionが動作する際のTimelineウィンドウ上の様々な情報が`ActionContext`として引数に入ってきます。

それぞれのプロパティ(フィールド)の詳細は以下の通りです。

|プロパティ(フィールド)名|注意事項など|
|:--|:--|
|`PlayableDirector director`|`TimelineEditor.inspectedDirector`と同様の内容が入ります。**Projectウィンドウで直接`TimelineAsset`を選択した状態だと`null`が入ります。**|
|`TimelineAsset timeline`|`TimelineEditor.inspectedAsset`と同様の内容が入ります。|
|`double? invocationTime`|コンテキストメニュー経由で実行した場合、右クリックした場所から時間を算出して秒数が入ります。※1 **ショートカット経由での実行の場合は`null`が入ります。**|
|`IEnumerable<TrackAsset> tracks`|`SelectionManager.SelectedTracks()`と同様の内容が入ります。※2|
|`IEnumerable<TimelineClip> clips`|`SelectionManager.SelectedClips()`と同様の内容が入ります。※2|
|`IEnumerable<IMarker> markers`|`SelectionManager.SelectedMarkers()`と同様の内容が入ります。※2|
※1 クリップが置かれる場所を右クリックした際はその位置の再生時間が入ってきましたが、左側にあるトラックリストの部分を右クリックしてコンテキストメニューから実行した際に入る時間がよく分からない値になっていました。調査中です。
※2 `SelectionManager`は`internal class`です。

### `public override bool Execute(ActionContext context)`

実際に処理したい内容をここに記述します。
引数には`Validate`メソッドのときと同様に`ActionContext`が入ってきます。

戻り値には処理の成否を返却します。

実行される**Actionのインスタンスは`ActionManager`という内部クラスでキャッシュされて使い回す**ため、フィールドに値を持ったりする場合は注意してください。

## `MenuEntryAttribute`

`TimelineAction`を継承したクラスにこの`Attribute`をつけることで、Timelineウィンドウ上のコンテキストメニューに処理を追加することができます。

機能としてはTimelineウィンドウの中でしか動作しないという点以外は`UnityEditor.MenuItem`とほぼ同様の内容です。

標準機能のプライオリティに関しては [`MenuPriority`](https://docs.unity3d.com/Packages/com.unity.timeline@1.4/api/UnityEditor.Timeline.Actions.MenuPriority.html)クラスで確認することができます。

`TimelineAction`の場合、**マーカー、トラック、クリップ(クリップが置かれていない場所を含む)を右クリックした際のコンテキストメニューに表示されます。**

※マーカーのみ、トラックのみ、クリップのみに対応したい場合はそれぞれ`MarkerAction`,`TrackAction`,`ClipAction`が存在します。

## `TimelineShortcutAttribute`

Timelineウィンドウ上でショートカットキーを使えるようにします。

機能としてはUnity2019から追加された[Shortcuts Manager](https://docs.unity3d.com/Manual/ShortcutsManager.html)と同様ですが、**このAttributeを使うことで自動的にTimelineのカテゴリに振り分けてくれます。**

コンテキストメニューで処理する内容はショートカットキーでも同様に使いたいケースが多いことから、この`TimelineAction`のサンプルで紹介されていますが、**このAttribute自体は`TimelineAction`と関係なく単独で動作します。**


ショートカットキー周りは時間があれば別途記事を作って紹介するかもしれません。。


## `Invoker`

とてもシンプルな名前ですが、**`TimelineAction`を含む各種`Action`クラスの処理を外から手軽に呼び出せるようにしたユーティリティクラス**です。

`TimelineAction`の場合、`bool InvokeWithSelected<T>() where T : TimelineAction`メソッドを使用することで、任意の独自Actionを呼び出すこと(ValidateしてExecuteするまで)が可能です。

引数に渡す`ActionContext`の用意やUndo処理なども含めて内部でやってくれるので、よほど特殊な事情がなければこの`Invoker`クラスを経由して各種Actionクラスを実行したほうがよいでしょう。

## オマケ

APIドキュメントにサンプルコードが書かれているのはとても有り難いことですが、内容は最低限しかないためイマイチどのように使うのかピンとこない人もいると思います。

実は標準的な機能(コピー＆ペーストや削除など)も、このActionを使って定義されています。
※場所は↓(@の後ろは自分の使っているTimelineのバージョン)
**`com.unity.timeline@1.4.0/Editor/Actions/TimelineActions.cs`**

`internal`なクラスを多用しているのでそのまま参考にできるわけではありませんが、それらの実装を見ることでイメージがかなり掴めるようになると思いますので、そちらも合わせて見てから自分の実装を進めると良いと思います。

# おわりに

今回は`TimelineAction`について解説してみました。
これ以外にも機能を限定した`MarkerAction`,`TrackAction`,`ClipAction`も存在しますが、基本的な仕様は`TimelineAction`と同様なので、この記事だけでも使えると思います。

Timelineもようやくコンテキストメニューも使えるようになり、拡張の幅もかなり広がってきました。
みなさんが開発しているツールが少しでも便利になってもらえれば幸いです。