【Unity】タイムライン風のAnimationEditorを作成する - UI編

本記事は [QualiArts Advent Calendar 2020](https://qiita.com/advent-calendar/2020/qualiarts)の２４日目の記事です。

## はじめに

### 内容
Editorを実装した際に躓いた点や新しく発見した点などを、個人的に後日振り替えれるようにまとめました。

### 想定される読者
- Unityエディタ拡張の初心者
- Animationに興味のある方

### 別記事
各内容が大きくなってしまったので記事をわけています。
興味のあるき時から読んでいただければ幸いです。

- [【Unity】タイムライン風のAnimationEditorを作成する - ロジック編](https://qiita.com/albatrus/items/2374da40919d78f0b81b)
- [UnityEditor拡張メモ](https://qiita.com/albatrus/items/b9799f7f2a805faf3da8)


## タイムライン風のEditorを作成する - UI編
この記事では主にUnityのEditor拡張でUIを作成したときの内容をまとめています。
Unityでゲームを作っているがツール周りの開発経験はほぼないため基本的な内容になります。

### 今回作りたいもの
- 再生ボタン
- InspectorでAnimation編集できる
- Animation追加ボタン
- 再生ライン

![image1_12_21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/9b196412-84b3-8a9e-4c83-8e0cb96d71f0.png)

次の画像を参考にUIを組んでいきました。

## 3つに描画エリア
まずはEditorを縦に分けている3つのエリアを作っていきます。
縦のエリアを分けるためには、EditorGUILayoutのVerticalScopeを使います。
具体的には、次のように実装をしました。

```c#
using (new EditorGUILayout.HorizontalScope())
{
    using (new EditorGUILayout.VerticalScope(GUILayout.Width(200f)))
    {
        // 左側のUI
    }

    using (new EditorGUILayout.VerticalScope(GUILayout.ExpandWidth(true)))
    {
        // 真ん中エリアのUI
    }

    using (new EditorGUILayout.VerticalScope(GUILayout.Width(300f)))
    {
        // 右エリアのUI
    }
}
```

参考画像 こんな感じに分かれる。
<img width="500" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/677e6413-e598-212d-4f94-443f2d6602b3.png">


## ツールバーを作成
タイムラインの再生や停止などタイムラインの操作をするためのボタンを配置していきます。
先程3つにわかれたエリアの上部を使います。

```c#
// 例再生ボタン
using (var scope = new EditorGUI.ChangeCheckScope())
{
    // 再生ボタン
    GUILayout.Toggle(_isPlay, _playTexture, "Toolbarbutton", GUILayout.Width(40f));
    if (scope.changed)
    {
        
    }
}
```

このような感じで必要なボタンやトグルを作っていきます。
とりあえず3つのボタンを配置するとこのような感じになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/4aa4148d-8d15-93d3-5bdc-15a20b453321.png)

## Menuの作成
ボタンを押したときのメニューを表示できるようにします。
想定されているのはTweenを追加・削除する場合ですが、いろいろな場面で使えそうなUIです。
GenericMenuを使って実装を行います。

##### 実装例
```c#
var menu = new GenericMenu();
menu.AddItem(new GUIContent("項目"), false, _ =>
{
  // Menuを押したときのアクション
},"");

menu.ShowAsContext();
```

ボタンを押したときにメニューが表示されるようになりました。
実際には、どのUIをアニメーションさせたいかのUIを指定したいです。

##### 画像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/abd7f2c1-2c88-9feb-3c92-11fc0459b615.png)

### 特定のエリア上のマウス右・左クリックでメニューを出す
ある特定の範囲内でマウスのボタンを押したときにメニューが表示されるようにしたい場合、次のような実装を行います。
マウスのクリック判定にはEvent.currentを使います。

```c#
using (new EditorGUILayout.VerticalScope(new GUIStyle("Box")))
{
    if (Event.current.type == EventType.MouseUp)
    {
        if (Event.current.button == 0)
        {
            // 左クリックを押した時
            Event.current.Use();

        }
        else if (Event.current.button == 1)
        {
            // 右クリックを押した時
            Event.current.Use();
        }
    }
}               

```

図はわかりにくいですが、このように特定の範囲でMenuを表示させることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/1af1c721-3fad-0606-b42e-ed19d2120a79.png)

ただこのままではEditor上のどこをクリックしてもメニューが表示されてしまうので、クリックできる範囲を指定する必要がありそうです。
その場合には次のように実装をしました。

```c#
if (Event.current.button == 0)
{
   var boxRect = GUILayoutUtility.GetLastRect();
   var mousePos = Event.current.mousePosition;
   // 特定の範囲内をクリックしたかどうか判定する
   if (boxRect.Contains(mousePos))
   {
       // 左クリックを押した時
       Event.current.Use();
   }
}
```


## タイムライン部分
真ん中のエリアに表示したいのがタイムラインのUIです。

### 時間の線を上部に表示
まずは、タイムライン上の現在の時間がわかるUIを表示したいと思います。
一定の距離の応じて線を引いて時間の区分を表します。
線はHandlesクラスを使って実装をしてみます。

```c#
var prev = Handles.color;
Handles.color = Color.white;

for (int i = 0; i <= lineCount; i++)
{
    var offsetPos = new Vector3(10.0f * i, 0f, 0f);
    // baseLinePosは起点のPosition
    var startLinePos = baseLinePos + offsetPos;
    var endLinePos = startLinePos;
    if (i % 5 == 0)
    {
        endPos += vector * 3.0f;
    }
    else
    {
        endPos += vector;
    }

    Handles.DrawLine(startPos, endPos);
}

Handles.color = prev;
```

#### Handles
Handlesは本来3D空間に図形を書くクラスなのですが2D画面上でも使うことができます。
DrawLineはスタート位置と終了位置を結ぶ線を描画します
「Handles.DrawLine」
https://docs.unity3d.com/ja/current/ScriptReference/Handles.DrawLine.html

#### 見た目
このような感じで線を引くことができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/407175da-0085-0a06-27a6-c7619d8e6c86.png)

時間がわかりやすいように数字を入れても良いかもしれません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/884dd821-6680-bb63-c496-d35783991000.png)


### 再生中のライン
タイムラインを再生しているときの現在の位置を確認するためのライン（縦線）を実装します。
これもHandlesのDrawLineを使います。

```c#
// 赤い線を引く
var prevColor = Handles.color;
Handles.color = Color.red;
var offset = 0.0f;
var startLinePos = new Vector3(offset,0.0f,0.0f);
var endLinePos = startPos + new Vector3(0.0f,bodyRect.height,0.0f);
Handles.DrawLine(startLinePos,endLinePos);
Handles.color = prevColor;
```

フラグを上手く管理して、再生ボタンを押すと赤い線が表示されるようするとそれっぽくなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/1f32f821-43e8-3fed-4cd0-fe6a74026b95.png)

#### Repaint
実際にはUpdateを使い再生中にoffsetの値を変更してあげれば動きます。
UpdateでRpaint処理を呼びUIを更新させる必要があります。

```c#
void Update()
{
    Repaint();
}
```

#### 見た目
![ezgif-3-fd08b80331e2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/98a4afdd-10a9-f721-e755-c959dbbabfdf.gif)


### Animation情報描画
タイムライン上にアニメーションの情報を描画します。
今回はTestとして開始時間のみのAnimation情報を用意しました。
これを使います。

```c#
// 描画の開始位置
Rect lineRect;

for (var i = 0; i < tweenAnimationInfos.Length; i++)
{
    var info = tweenAnimationInfos[i];
    var boxRect = lineRect;
    
    // 開始位置
    boxRect.x = info.StartTweenTime / 0.02f * 10.0f - 1.0f;
    var animationDuration = 1.0f; // 長さは固定

    // 終わりの位置
    boxRect.width = animationDuration / 0.02f * 10.0f + 1.0f;
    EditorGUI.DrawRect(boxRect,Color.gray);

    lineRect.y += lineRect.height + 5.0f;
}

```

#### 見た目

このような感じで描画されます。
現状、スタート時間しか変更できないですが長さなどを変更できるようすれば連続したAnimationを実装できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/e469b2f8-13f6-9b35-d707-639171f546d5.png)


## Inspector的なもの作成
再生するアニメーション情報を表示・編集するためにInspectorを右側のエリアに表示をします。
この場合、Editor.CreateEditorを使うと上手く表示ができました。
[参考]
https://baba-s.hatenablog.com/entry/2017/12/29/095000

```c#
var editor = Editor.CreateEditor(TweenInfo);
if (editor != null)
{
    editor.OnInspectorGUI();
}
```

このような感じでSerializeFieldやpublicな変数がEditor上に表示できます。
<img width="400" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/61ab772e-f79e-d219-17bb-d2550cf10b67.png">


## 最後に
Editorに関しては、この記事執筆時（2020/12/24)ある程度動くところまで実装はできたのですが、まだゲーム開発で使えるまでには作り込めていないため、サンプルなどは用意していません。
今後継続して開発は続けるつもりですので、何かしらアウトプットできるものがあれば何かしらの形で公開したいと思っています。

### 今後実装をしていきたいこと。
現在、本当に基本的な部分までしか実装できていないので、今後実装をしてみたい機能を列挙してみました。

- Easingのあるアニメーション
- アニメーション設定を保存・読み込み
- タイムライン部分のUIの拡大縮小
- UIToolKitによる実装

エディタ拡張のUIについては今後UITookKitに置き換わっていくと思うので、今回は従来のUI実装をやってみましたが、UITookKitでも実装をしてみたいなと思っています。
https://docs.unity3d.com/ja/2020.1/Manual/UIElements.html

#### 関連する記事
- [【Unity】タイムライン風のAnimationEditorを作成する - ロジック編](https://qiita.com/albatrus/items/2374da40919d78f0b81b)
- [UnityEditor拡張メモ](https://qiita.com/albatrus/items/b9799f7f2a805faf3da8)