- わかる
- コードをよむ
- わからない
- つぎにすすむ
なにを言っているのか
ObjectPool
を用いて頻繁にHumanoid
のAnimator
で動くキャラの出現/消失を行います。
そのとき、新しく出したキャラも既に出ているキャラの動きに合わせたいです。
なにも考えずキャラを出し入れするとこんな感じになります。
統率の取れていないオークの群れなどお話になりませんね。
Animator
はdisable
されると再生位置がリセットされます。そのため、ObjectPool
に回収されるとアニメーションが最初からになってしまいます。ObjectPool
の要素が足りなくなって新規作成されたキャラも同様です。
Animatorの再生位置をシンクロさせる
解説
動きを揃えるからにはまず基準を定めなければなりません。というわけではぴねこちゃんをオークリーダーに任命します。はぴねこちゃんにもオークにも同じAnimator
をセットします。
アニメーションはある程度長くて動きが大きいものならズレがわかりやすいので、ユニティちゃん Candy Rock Star ライブステージ!のダンスを使います。
Animatorのアニメーションを途中から再生するには以下のメソッドを使います。
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のアニメーションはいつ更新されているのか。イベント実行順を見てみます。
Physics
ブロックとGame Logic
ブロックそれぞれにInternal Animation Update
の記載があります。この2つが実行される前にパラメータをセットしておけば大丈夫そうです。というわけでFixedUpdate
。
ただし、同じFixedUpdate
のタイミングで「オークリーダーからstateを取得」→「各オークに通知」する必要があります。異なるComponent
間の実行順は保証されていないので一工夫……しないでUniRx
のSubject
を使いましょう。
// オークリーダーの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
から出し入れもする。デブクラス。
めんどくせえからやってないけどちゃんと別クラスに分割しましょう。
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
各オークにアタッチされています。
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の動画を参考資料としていろいろ見たのですが、みんなとんでもねえ性癖でとんでもねえなって思いました。
おしまい。
素材
ダンス
ユニティちゃんライブステージ! -Candy Rock Star-
© Unity Technologies Japan/UCL
背景
Farland Skies - Cloudy Crown
参考
Animatorでアニメーションを途中から再生する
これが知りたかった。
UniRxのObjectPoolを利用する
くわしい。
【Unity】Animatorの更新タイミングを変更する
これをなんかうまいことすればずれる問題も解決するような気がしないでもないが、毎フレームセットしたところでパフォーマンスへの負荷も大きくないので調べる気がない。