ゴマちゃんフロンティア

アザラシが大好きなエンジニアの開発日記です

【Unity】AnimationControllerでループするステートから一定確率で別のモーションを実行する方法

time 2020/08/24

今回は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()`で判定しちゃだめなの?

アニメーションの再生がループしたタイミングではOnStateExit()が呼ばれません。なので「現在のループの再生が終わった」の判定をOnStateUpdate()内で行う必要があります。
具体的にはstateInfo.normalizedTime1で割ったあまりを使います。これはnormalizedTimeがループしている限り増え続けるためです。
1で割ったあまりは0~1の間になるので、「現在のループのどの再生位置か」が大まかにわかります。

例えば「4回目のループで再生位置が半分」だった場合、`normalizedTime`は3.5で、1で割ったあまりは0.5になるよ!

なので再生位置の後ろのほうで乱数を生成し、引っかかったらAnimator.Play()で待機モーションを実行します。サンプルでは0.9以上としました。
また何度も判定されることを避けるため、ループの最初のほうでフラグをfalseにし、判定後にtrueにして「判定は1ループ1回のみ」にしました。

設定後はIdleステートのインスペクターからidleMotionPropをお好みの値にします。私の場合は30 (=30%の確率で実行) にしています。

これでうまくいけば、待機中にたまーに待機モーションが再生されます。

綺麗なやり方かと言われると微妙ですが、キャラクター本体側のスクリプトを汚さず、また複数のAnimatorControllerで使いまわせるので、これはこれで良しとします。

down

コメントする