【Unity】タイムライン風のAnimationEditorを作成する - ロジック編

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

## はじめに
この記事ではUnityのEditorを実装した際に躓いた点や新しく発見した点などを、個人的に後日振り替えれるようにまとめました。

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

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

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


## クラス構成
今回次のクラスを作成しEditorを実装しました。
クラスの役割について説明します。

### TweenAnimation
GameObjectへアタッチするコンポーネント。
タイムラインはこのTween単位で作成される。
具体的なTween情報を保持する。
Tween情報(TweenInfo)は複数設定が可能。

### TweenInfo
Tweenの情報を持つクラス
 - Tweenのスタート時間
 - 具体的なTweenの中身

### Tween
実際のTweenアニメーションの情報を司るクラス。
- 長さ
- 経過時間
- スタート値
- 終了値
などを共通に持ち、継承したくらすで振る舞い（移動、拡縮、回転、透明...)などを制御する。
経過時間を使って「スタート値」「終了値」間の値を取得します。

## Tweenを追加する
TweenAnimation直下のTransformを取得しMenuが表示できるようにします。
その際にGenericMenuクラスを利用します。

```c#
var menu = new GenericMenu();
var targetGameObjectTransforms = _tweenAnimation.GetComponentsInChildren<Transform>(false);
foreach (var transform in targetGameObjectTransforms)
{
    var targetTransform = transform;
    menu.AddItem(new GUIContent(targetTransform.name), false, _ =>
    {
       // こちらの追加処理
    }
}
```

メニューを選択したときに対象のTransformにTweenが追加されるようにします。
エディタ上のTweenを追加するときには「Undo.AddComponent」を利用します。
https://baba-s.hatenablog.com/entry/2019/09/30/170000

```c#
// 選択したTweenBehaviourを対象のTweenAnimationへ追加する
var tween = Undo.AddComponent<Tween>(targetTransform.gameObject);
tween.hideFlags = HideFlags.HideInInspector;
 
// SerializeObjectを更新
_animationSerializeObject.Update();
var tweenAnimationInfos = _animationSerializeObject.FindProperty(TweenAnimationInfosString);
tweenAnimationInfos.arraySize++;

// 追加する
var tweenInfo = tweenAnimationInfos.GetArrayElementAtIndex(tweenAnimationInfos.arraySize - 1);
tweenInfo.FindPropertyRelative("TweenBase").objectReferenceValue = tween;
tweenInfo.FindPropertyRelative("StartTweenTime").floatValue = 0.0f;
_animationSerializeObject.ApplyModifiedProperties();
```

### 削除する
逆に削除する場合にはこのような形で実装ができます。

```c#
_animationSerializeObject.Update();
var tweenInfos = _animationSerializeObject.FindProperty(TweenAnimationInfosString);
for (int i = 0; i < tweenInfos.arraySize; i++)
{
    var info = tweenInfos.GetArrayElementAtIndex(i);
    if (info.FindPropertyRelative("TweenBase").objectReferenceValue != targetTween)
    {
        continue;
    }

    // 削除する
    tweenInfos.DeleteArrayElementAtIndex(i);
    i--;
}

_animationSerializeObject.ApplyModifiedProperties();

Undo.DestroyObjectImmediate(targetTween);
```


## タイムラインの経過時間に合わせてTweenを動かす
各Tweenに関してはスタート時間がバラバラになる可能性があるので、単純にタイムライン上の時間を使うことができません。
そのため各Tweenごと経過時間を計算します。

### タイムライン上での経過時間を取得する
まずはタイムライン上の経過時間を取得します。

```c#
private TweenAnimation _tweenAnimation;
private float _prevTime;

void Update()
{
    if(isPlay)
    {
         var currentTime = Time.realtimeSinceStartup;
         var deltaTime = (currentTime - _prevTime) * 1.0f;
         // Durationはアニメーションの長さ
         var deltaRate = deltaTime / _tweenAnimation.Duration;
         // CurrentTimeRateは経過時間の割合
         var rate = _tweenAnimation.CurrentTimeRate + deltaRate;
         if(rate > 1.0f)
         {
            rate = 1.0f;
            isPlay = false;
         }
         
         _tweenAnimation.SetCurrentTimeRate(rate);
         _prevTime = currentTime;
}
```

### 各Tweenに経過時間を設定する
タイムライン経過時間を取得できたので、これを使って各Tweenの経過時間設定します。
先程のコードでは SetCurrentTimeRate でTweenAnimationのRateを設定しているので、これを使います

```c#
// TweenAnimationの経過時間割合を設定
public void SetCurrentTimeRate(float rate)
{
    _currentTimeRate = rate;
}
```

### エディタ上の再生位置を更新
TweenAnimationに設定されているTweenの情報を元に現在時間とスタート時間・Tweenの長さを加味して再生中なのかどうかを判定しています。
再生中の場合にTweenに経過時間をセットしています。

```c#
// TweenAnimationの経過時間割合を設定
public void SetCurrentTimeRate(float rate)
{
    _currentTimeRate = rate;

    for (int i = 0; i < _tweenAnmationInfos.Length; i++)
    {
        var targetInfo = _tweenAnmationInfos[i];
        if (targetInfo.TweenBase == null)
        {
            continue;
        }
    
        var tweenDuration = targetInfo.TweenBase.Duration;

        // 現在時間がスタートタイムより前の場合
        if (currentTime < targetInfo.StartTweenTime)
        {
            return;
        }
        // 現在時間がTweenの時間を超えている場合
        if (currentTime >= targetInfo.StartTweenTime + tweenDuration)
        {
            return;
        }

        // Tweenへ
        float time = tweenDuration > float.Epsilon ? (currentTime - targetInfo.StartTweenTime) : 0.0f;
        targetInfo.TweenBase.SetCurrentTime(time);
    }
}
```

### 再生時間を元に任意のAnimationを実行する
Tweenクラスを継承して、移動や透過などのそれぞれAnimationを担当するクラスを作り、経過時間に応じて値を変更します。

```c#
public override void SetCurrentTime(float currentTime)
{
    // キャッシュ
    _currentTime = currentTime;

    // 経過時間割合算出
    var rate = currentTime / _duration;
    rate = Mathf.Min(rate, 1.0f);
            
    // 内部処理
    SetCurrentTimeRate(rate);
}
```

###  経過時間に応じた値を取得する

```c#
// timeRateが経過時間
var value = Mathf.Lerp(StartValue, EndValue, timeRate);
```


例えば、CanvasGroupを使った透過処理の場合には次のようなコードを書くことができます。

```c#

// 個別の内部処理
protected void SetCurrentTimeRateInternal(float rate)
{
    var value = Mathf.Lerp(0.0f, 1.0f, rate);
    if (_canvasGroup != null)
    {
        _canvasGroup.alpha = value;
    }
}
```

## Tweenを選択する
特定のTweenを選択する場合には次のように実装ができます。

### 選択するKeyを設定
まずはGUI.SetNextControlName()を使って選択に必要なkey(String型）を設定します。

```c#
// 今回はTweenのIndex
GUI.SetNextControlName(tweenIndex.ToString());
```

### 選択する
設定したKeyを選択する場合にはGUI.FocusControl()を使います。
例えば対象のTweenをマウスでクリックした時など何かしらのイベントに仕込みます。

```c#
if (Event.current.type == EventType.MouseUp)
{
    if (Event.current.button == 0)
    {
        // 左判定
        var tweenRect = GUILayoutUtility.GetLastRect();
        var mousePosition = Event.current.mousePosition;
        if (tweenRect.Contains(mousePosition))
        {
            // Indexをフォーカスする
            GUI.FocusControl(tweenIndex.ToString());
            Event.current.Use();
        }
    }
}
```

### 選択したTweenを取り出す
選択されたkeyを取り出すときにはGUI.GetNameOfFocusedControl()を使います。
今回int型のString型にキャストして使っているので、少しめんどくさいやり方にはなっています。

```c#
var focusIndexString = GUI.GetNameOfFocusedControl();
if (!string.IsNullOrEmpty(focusIndexString))
{
    int focusIndex = int.Parse(focusIndexString);
    if ( _tweenAnimation.TweenAnmationInfos.Length > focusIndex)
    {
        _focusedTweenAnimationInfo = _tweenAnimation.TweenAnmationInfos[focusIndex];
    }
}
```

取り出したTween情報を元に、色を変えたり、Inspectorを表示したりします。


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

### 関連記事

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