ObjectPoolから出し入れするAnimatorの再生状態をシンクロさせてオークをたくさんダンスさせる

![ezgif-1-abc5260e8697.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146245/0cb74090-be88-9e89-9e5e-835fe64c58e8.gif)

<dl>
  <dt>わかる</dt>
  <dd><a href="https://qiita.com/nekomimi-daimao/items/82c5cb795d824199efd2#コード">コードをよむ</a></dd>
  <dt>わからない</dt>
  <dd>つぎにすすむ</dd>
</dl>


## なにを言っているのか

`ObjectPool`を用いて頻繁に`Humanoid`の`Animator`で動くキャラの出現/消失を行います。
そのとき、新しく出したキャラも既に出ているキャラの動きに合わせたいです。

なにも考えずキャラを出し入れするとこんな感じになります。

![fail.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146245/43ce6da2-22ca-ed19-dddc-7f38f3d2efcc.gif)

統率の取れていないオークの群れなどお話になりませんね。
`Animator`は`disable`されると再生位置がリセットされます。そのため、`ObjectPool`に回収されるとアニメーションが最初からになってしまいます。`ObjectPool`の要素が足りなくなって新規作成されたキャラも同様です。

## Animatorの再生位置をシンクロさせる

### 解説
動きを揃えるからにはまず基準を定めなければなりません。というわけで[はぴねこちゃん](https://assetstore.unity.com/packages/3d/characters/harpycat-121122)をオークリーダーに任命します。はぴねこちゃんにもオークにも同じ`Animator`をセットします。
アニメーションはある程度長くて動きが大きいものならズレがわかりやすいので、[ユニティちゃん Candy Rock Star ライブステージ！](https://youtu.be/M_GUlXJ52YA)のダンスを使います。

Animatorのアニメーションを途中から再生するには以下のメソッドを使います。

```C#:Animator.Play
public void Play (int stateNameHash, int layer= -1, float normalizedTime= float.NegativeInfinity);
```
`stateNameHash`がアニメーションの種類、`normalizedTime`はアニメーションの再生位置です。`layer`は特に変更しないので初期値の-1を入れます。
このふたつのパラメータをオークリーダーのAnimatorから取得できればよいわけです。取得の仕方は以下の通り。

```c#
var currentState = _masterAnimator.GetCurrentAnimatorStateInfo(0);
var stateHash = currentState.fullPathHash;
var normalizedYime = currentState.normalizedTime;
```

問題はこの値を取得するタイミングです。Animatorのアニメーションはいつ更新されているのか。イベント実行順を見てみます。
<details><summary>Unity公式 : [イベント関数の実行順](https://docs.unity3d.com/ja/2019.1/Manual/ExecutionOrder.html)
</summary>
![monobehaviour_flowchart.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146245/08b3267c-3990-d079-08a6-13c8a93b0364.png)</details>

`Physics`ブロックと`Game Logic`ブロックそれぞれに`Internal Animation Update`の記載があります。この２つが実行される前にパラメータをセットしておけば大丈夫そうです。というわけで`FixedUpdate`。
ただし、同じ`FixedUpdate`のタイミングで「オークリーダーからstateを取得」→「各オークに通知」する必要があります。異なる`Component`間の実行順は保証されていないので一工夫……しないで`UniRx`の`Subject`を使いましょう。

```c#:OrcSecurer
// オークリーダーのAnimator
[SerializeField] private Animator _masterAnimator;
// オークリーダーの初期位置
private Vector3 _masterStartPosition;

// 初期位置からの差分, stateNameHash, normalizedTime
public readonly Subject<(Vector3, int, float)> MasterInfo = new Subject<(Vector3, int, float)>();

void Start()
{
    _masterStartPosition = _masterAnimator.rootPosition;

    Observable.EveryFixedUpdate().TakeUntilDestroy(_masterAnimator.gameObject).Subscribe(l =>
    {
        var currentDiff = _masterAnimator.rootPosition - _masterStartPosition;
        var currentState = _masterAnimator.GetCurrentAnimatorStateInfo(0);

        MasterInfo.OnNext((currentDiff, currentState.fullPathHash, currentState.normalizedTime));
    });
}
```

後はこのクラスを`Singleton`にしておきます。そして`ObjectPool`から取り出されたときに各オークはこの`Subject`を購読すればいいわけです。

```c#:各オークの初期化時
OrcSecurer.Instance.MasterInfo.TakeUntilDisable(this).Subscribe(tuple =>
{
    _animator.gameObject.transform.position = _startPosition + tuple.Item1;
    _animator.Play(tuple.Item2, -1, tuple.Item3);
});
```
これで無事同じ`FixedUpdate`のタイミングで「オークリーダーからstateを取得」→「各オークに通知」をすることができました。

説明するタイミングを逃しましたが、同時に流している`Vector3`は初期位置から`Apply Root Motion`によってどれだけ移動したか……の差分です。アニメーションの再生位置をすっとばすことで、それまでアニメーションによって移動したはずの座標位置もすっとばされています。なので合わせて補正してあげないといけません。

また、もしUniRxが使えない場合は`[DefaultExecutionOrder()]`属性でオークリーダーからstateを取得するクラスの実行優先度を高めて先にstateを取得します。`Singleton`にしておくことで自由にアクセスできるようになるので、各オークにアタッチされた`Component`から取得しましょう。[^wakaran]

主題はこれで終わりです。改めて最初のgifを見てください。ちゃんとシンクロしているのがわかる[^wakaru]と思います。

### コード

#### OrcSecurer
オークリーダーはぴねこちゃんのstateを取得するクラス。同時にオークを`ObjectPool`から出し入れもする。デブクラス。
めんどくせえからやってないけどちゃんと別クラスに分割しましょう。

```C#:OrcSecurer
using System;
using System.Collections.Generic;
using UniRx;
using UniRx.Toolkit;
using UnityEngine;

public class OrcSecurer : SingletonMonoBehaviour<OrcSecurer>
{
    [SerializeField] private Animator _masterAnimator;
    [SerializeField] private AnimatorSynchronizer _orcPrefab;

    private const int AppearCount = 20;
    public readonly Subject<(Vector3, int, float)> MasterInfo = new Subject<(Vector3, int, float)>();

    private Vector3 _masterStartPosition;

    void Start()
    {
        _masterStartPosition = _masterAnimator.rootPosition;

        Observable.EveryFixedUpdate().TakeUntilDestroy(_masterAnimator.gameObject).Subscribe(l =>
        {
            var currentDiff = _masterAnimator.rootPosition - _masterStartPosition;
            var currentState = _masterAnimator.GetCurrentAnimatorStateInfo(0);

            MasterInfo.OnNext((currentDiff, currentState.fullPathHash, currentState.normalizedTime));
        });

        var currentAppearList = new List<AnimatorSynchronizer>(AppearCount);
        var orcPool = new OrcPool(_orcPrefab, this.transform);

        for (var count = 0; count < AppearCount; count++)
        {
            var orc = orcPool.Rent();
            orc.Ready();
            currentAppearList.Add(orc);
        }

        Observable.Interval(TimeSpan.FromMilliseconds(400)).TakeUntilDestroy(this).Subscribe(
            l =>
            {
                if (UnityEngine.Random.Range(0, 2) == 0 && currentAppearList.Count < AppearCount * 2)
                {
                    var orc = orcPool.Rent();
                    orc.Ready();
                    currentAppearList.Add(orc);
                    return;
                }

                var index = UnityEngine.Random.Range(0, currentAppearList.Count);
                var removeOrc = currentAppearList[index];
                currentAppearList.RemoveAt(index);
                orcPool.Return(removeOrc);
            },
            () => { orcPool.Dispose(); });
    }

    private class OrcPool : ObjectPool<AnimatorSynchronizer>
    {
        private readonly AnimatorSynchronizer _prefab;
        private readonly Transform _parent;

        public OrcPool(AnimatorSynchronizer prefab, Transform parent)
        {
            _prefab = prefab;
            _parent = parent;
        }

        protected override AnimatorSynchronizer CreateInstance()
        {
            return GameObject.Instantiate(_prefab, _parent);
        }

        protected override void OnBeforeRent(AnimatorSynchronizer instance)
        {
            instance.gameObject.transform.position = new Vector3(0, -10, 0);
            base.OnBeforeRent(instance);
        }

        protected override void OnBeforeReturn(AnimatorSynchronizer instance)
        {
            base.OnBeforeReturn(instance);
            instance.gameObject.transform.position = new Vector3(0, -10, 0);
        }
    }
}
```

#### AnimatorSynchronizer
各オークにアタッチされています。

```C#:AnimatorSynchronizer
using UniRx;
using UnityEngine;

public class AnimatorSynchronizer : MonoBehaviour
{
    [SerializeField] private Animator _animator;

    private Vector3 _startPosition;
    private readonly float[] SquarePoints = {-3f, -1.5f, 0, 1.5f, 3f};

    public void Ready()
    {
        SetupStartPosition(new Vector3(SquarePoints[Random.Range(0, SquarePoints.Length)], 0f, SquarePoints[Random.Range(0, SquarePoints.Length)]));
    }

    public void SetupStartPosition(Vector3 startPosition)
    {
        _startPosition = startPosition;

        OrcSecurer.Instance.MasterInfo.TakeUntilDisable(this).Subscribe(tuple =>
        {
            _animator.gameObject.transform.position = _startPosition + tuple.Item1;
            _animator.Play(tuple.Item2, -1, tuple.Item3);
        });
    }
}

```

## 余録
### ObjectPool
```C#
protected override void OnBeforeRent(AnimatorSynchronizer instance)
{
    instance.gameObject.transform.position = new Vector3(0, -10, 0);
    base.OnBeforeRent(instance);
}

protected override void OnBeforeReturn(AnimatorSynchronizer instance)
{
    base.OnBeforeReturn(instance);
    instance.gameObject.transform.position = new Vector3(0, -10, 0);
}
```
なぜインスタンスに対して`ObjectPool`出入りのときに変な座標をセットしているかというと、`ObjectPool`から出て`enable`になったキャラが指定された座標に移動する前に一瞬だけ見えてしまうからです。かなりちらついて気になります。新しく生成されたキャラも座標は`(0, 0, 0)`なのでこちらも同様です。
出現の瞬間にプレイヤーから見えなければいいわけなので、`ObjectPool`の中にいる間は地の底に沈めることで誤魔化しています。どうせ`disable`なのでどこにいてもいいでしょう。たぶん。

あと、書いていて思ったのですが、`ObjectPool`は別クラスにするよりプーリングするクラスの中にサブクラスとして書いちゃったほうがわかりやすくていいかもしれません。行数が増えすぎるかもですが。

### Animator
Unity歴1年ちょっと、ありものを使うばかりで`Animation`関係を触ったことなかったんですが、なんか、これ、闇、深い……？
いろいろ便利にラッピングされているっぽくて、奥底に何が潜んでいるかわからない恐怖を感じる。

毎フレームパラメータをセットしているのが気持ち悪くて、というかそもそも最初のアニメーション開始位置さえ合わせちゃえば後はみんな同じなのでは？　と思って最初の10フレームだけ補正をかけるなどしたのですが、じわじわずれてきます。
`Update`にタイミングを変更してもだめ。UniRxではなく`MonoBehaviour`のコールバックを使ってもだめ。
ということは肉眼で検知できないレベルでオークリーダーとオークのダンスがずれている？[^kishi]　謎。
あと、ステートマシーンによるアニメーションの遷移ですが、これみんなよく管理できてますね……つらい……なんだこれは……。

## まとめ
自分で作る前にUnityでダンス動画とか作ってる人のブログ漁ったらよかったかも。同じことを既に実行済みの人がいるはず。
あと、MMDの動画を参考資料としていろいろ見たのですが、みんなとんでもねえ性癖でとんでもねえなって思いました。

おしまい。

## 素材
はぴねこちゃん[^harpy]
[HarpyCat](https://assetstore.unity.com/packages/3d/characters/harpycat-121122)

オーク[^sale]
[POLYGON - Fantasy Rivals](https://assetstore.unity.com/packages/3d/characters/humanoids/polygon-fantasy-rivals-118399)

ダンス
[ユニティちゃんライブステージ！ -Candy Rock Star-](http://unity-chan.com/download/releaseNote.php?id=CandyRockStar)
© Unity Technologies Japan/UCL

背景
[Farland Skies - Cloudy Crown](https://assetstore.unity.com/packages/2d/textures-materials/sky/farland-skies-cloudy-crown-60004)

## 参考
[Animatorでアニメーションを途中から再生する](https://kimoota.wiki.fc2.com/wiki/Unity%2FTips#tf08fb1f)
これが知りたかった。

[UniRxのObjectPoolを利用する](https://qiita.com/toRisouP/items/2a5fb86654525a4a8453)
くわしい。

[【Unity】Animatorの更新タイミングを変更する](http://redfreshet.com/2015/12/28/unity_animator_update-mode/)
これをなんかうまいことすれば[ずれる問題](#animator)も解決するような気がしないでもないが、毎フレームセットしたところでパフォーマンスへの負荷も大きくないので調べる気がない。


[^wakaru]: あなたも「わかる」側になりました
[^wakaran]: たぶんうまくいくと思うけどあんまり自信ない
[^kishi]: こんなザマじゃ女騎士サマに勝てねえよなぁ？
[^harpy]: 「harpy」ってそらで書けたのですが、いったいどこで綴り覚えたんだっけ……
[^sale]: セールで買ったけど使ってなかったからつい