LoginSignup
2
3

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-05-12

ezgif-1-abc5260e8697.gif

わかる
コードをよむ
わからない
つぎにすすむ

なにを言っているのか

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

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

fail.gif

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

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

解説

動きを揃えるからにはまず基準を定めなければなりません。というわけではぴねこちゃんをオークリーダーに任命します。はぴねこちゃんにもオークにも同じAnimatorをセットします。
アニメーションはある程度長くて動きが大きいものならズレがわかりやすいので、ユニティちゃん Candy Rock Star ライブステージ!のダンスを使います。

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

Animator.Play
public void Play (int stateNameHash, int layer= -1, float normalizedTime= float.NegativeInfinity);

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

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

問題はこの値を取得するタイミングです。Animatorのアニメーションはいつ更新されているのか。イベント実行順を見てみます。

Unity公式 : イベント関数の実行順

monobehaviour_flowchart.png

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

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を購読すればいいわけです。

各オークの初期化時
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から取得しましょう。1

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

コード

OrcSecurer

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

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

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

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

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のコールバックを使ってもだめ。
ということは肉眼で検知できないレベルでオークリーダーとオークのダンスがずれている?3 謎。
あと、ステートマシーンによるアニメーションの遷移ですが、これみんなよく管理できてますね……つらい……なんだこれは……。

まとめ

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

おしまい。

素材

はぴねこちゃん4
HarpyCat

オーク5
POLYGON - Fantasy Rivals

ダンス
ユニティちゃんライブステージ! -Candy Rock Star-
© Unity Technologies Japan/UCL

背景
Farland Skies - Cloudy Crown

参考

Animatorでアニメーションを途中から再生する
これが知りたかった。

UniRxのObjectPoolを利用する
くわしい。

【Unity】Animatorの更新タイミングを変更する
これをなんかうまいことすればずれる問題も解決するような気がしないでもないが、毎フレームセットしたところでパフォーマンスへの負荷も大きくないので調べる気がない。


  1. たぶんうまくいくと思うけどあんまり自信ない 

  2. あなたも「わかる」側になりました 

  3. こんなザマじゃ女騎士サマに勝てねえよなぁ? 

  4. 「harpy」ってそらで書けたのですが、いったいどこで綴り覚えたんだっけ…… 

  5. セールで買ったけど使ってなかったからつい 

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3