2020/11/09
今回はUnityのAnimatorに関するちょっとしたお話です。
つい最近、開発中のゲームで所謂「待機モーション」を実装しました。何もしないでいるとたまに周りを見渡したり、武器を構えなおしたりするあれです。
Unity的なやり方はあれこれありますが、私は「待機中(Idle)ステートがループした際に一定確率で実行する」ようにしました。
まずは待機モーションをIdleMotionという名前のステートで作成します。
Animator.Play()
で実行するため、Idleからの遷移条件は設定していません。逆にモーション終了後は自動的にIdleに戻るようにします。
次にStateMachineBehaviourを継承したスクリプトを作成し、Idleステートに設定します。
using UnityEngine; public class Idle : StateMachineBehaviour { [SerializeField] [Range(0, 100f)] private float idleMotionProp = 0; private bool executeFlag = false; public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { // ループするので1で割った余りをnormalizedTimeとして使用 if (executeFlag && (stateInfo.normalizedTime % 1) <= 0.1f) { executeFlag = false; } if (!executeFlag && 0.9f <= (stateInfo.normalizedTime % 1)) { executeFlag = true; var executeProp = Random.Range(0, 100f); if (executeProp <= idleMotionProp) { animator.Play("IdleMotion"); } } } }
ポイントは「OnStateUpdate()
内で判定する」ことです。
アニメーションの再生がループしたタイミングではOnStateExit()
が呼ばれません。なので「現在のループの再生が終わった」の判定をOnStateUpdate()
内で行う必要があります。
具体的にはstateInfo.normalizedTime
を1で割ったあまりを使います。これはnormalizedTime
がループしている限り増え続けるためです。
1で割ったあまりは0~1の間になるので、「現在のループのどの再生位置か」が大まかにわかります。
なので再生位置の後ろのほうで乱数を生成し、引っかかったらAnimator.Play()
で待機モーションを実行します。サンプルでは0.9以上としました。
また何度も判定されることを避けるため、ループの最初のほうでフラグをfalseにし、判定後にtrueにして「判定は1ループ1回のみ」にしました。
設定後はIdleステートのインスペクターからidleMotionProp
をお好みの値にします。私の場合は30 (=30%の確率で実行) にしています。
これでうまくいけば、待機中にたまーに待機モーションが再生されます。
綺麗なやり方かと言われると微妙ですが、キャラクター本体側のスクリプトを汚さず、また複数のAnimatorControllerで使いまわせるので、これはこれで良しとします。