【Unity】【Animator Controller】即座に遷移できなかったら無効になるトリガーを作る

UnityのAnimator Controllerで即座に遷移できなかったら無効になるトリガーを作る方法をまとめました。

Unity2019.3.0

はじめに

さてAnimator Controllerでは状態遷移の条件にトリガーを使うことができます。
今これを使って、待機状態からJumpトリガーでJumpステートに遷移するステートマシンを考えます。

f:id:halya_11:20200213155825p:plain

ここで問題になるのが「トリガーを有効化するとそのトリガーを使って遷移が行われたタイミングで無効になる」という性質です。
この性質により、Jump状態中にJumpトリガーが設定されると、Jump状態の終了を待ってからまたJump状態に入ってしまいます。

f:id:halya_11:20200213160622p:plain

この挙動は望ましい場合もありますが、多くの場合望ましくありません。
トリガーを設定した時点ですぐに遷移できなかったら、トリガーを無効にしてほしいところです。

そこでこの記事では、即座に遷移できなかった場合に無効になるトリガーの仕組みを作成します。

遷移できなかったらトリガーを無効にする

これを実現するにはMonoBehaviour.OnAnimatorMove()にトリガーのリセット処理を書きます。

using UnityEngine;

public class Example : MonoBehaviour
{
    private Animator _animator;

    void Start()
    {
        _animator = GetComponent<Animator>();
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.J)){
            _animator.SetTriggerOneFrame("Jump");
        }
    }
    
    private void OnAnimatorMove()
    {
        _animator.ResetTrigger("Jump");
    }
}

MonoBehaviour.OnAnimatorMove()はステートマシンやアニメーションが評価された後に呼ばれるイベント関数です。

docs.unity3d.com

これにより、次回ステートマシンが評価された後に強制的にトリガーが無効化されます。

拡張メソッドで実現する

前節の方法はLateUpdateでトリガーがセットされることも考慮した実装になっていますが、
Updateでしかトリガーをセットしない制約を設けるのであれば拡張メソッドでもっと手軽に使える実装ができます。

public static class AnimatorExtension{

    public static void SetTriggerOneFrame(this Animator self, MonoBehaviour monoBehaviour, string name)
    {
        IEnumerator SetTriggerOneFrame(Animator animator, string triggerName) {
            animator.SetTrigger(triggerName);
            yield return null;
            // 1フレーム後のUpdate後にトリガーをリセットする
            if (animator != null) {
                animator.ResetTrigger(triggerName);
            }
        }
        monoBehaviour.StartCoroutine(SetTriggerOneFrame(self, name));
    }

    public static void SetTriggerOneFrame(this Animator self, MonoBehaviour monoBehaviour, int id)
    {
        IEnumerator SetTriggerOneFrame(Animator animator, int triggerId) {
            animator.SetTrigger(triggerId);
            yield return null;
            // 1フレーム後のUpdate後にトリガーをリセットする
            if (animator != null) {
                animator.ResetTrigger(triggerId);
            }
        }
        monoBehaviour.StartCoroutine(SetTriggerOneFrame(self, id));
    }
}

使う側は以下のように書くだけです。

_animator.SetTriggerOneFrame(this, "Jump");

上記の実装ではメソッド実行時にトリガーをセットし、次回のUpdate後にこのトリガーをリセットします。
イベント関数の実行順を見ればわかるように、もしLateUpdateでこのメソッドを実行した場合には
ステートマシンによる状態遷移が評価される前にトリガーがリセットされてしまいます。

docs.unity3d.com

どちらにしろLateUpdateでトリガーをセットするのは適切な実装ではないケースが多いかと思うので、
個人的にはトリガーのセットはUpdateで行うという制約を設けたうえでこの拡張メソッドを使うのは良い手だと思います。

UniRxを使ってさらに簡潔に

さらにUniRxを使えば引数のMonoBehaviourすら渡す必要がなくなり、SetTrigger()と同じ感覚で使えるようになります。

public static class AnimatorExtension{

    public static void SetTriggerOneFrame(this Animator self, string name)
    {
        self.SetTrigger(name);
        Observable
            .NextFrame()
            .Subscribe(_ => {}, () => {
                // 1フレーム後のUpdate後にトリガーをリセットする
                if (self != null) {
                    self.ResetTrigger(name);
                }
            });
    }

    public static void SetTriggerOneFrame(this Animator self, int id)
    {
        self.SetTrigger(id);
        Observable
            .NextFrame()
            .Subscribe(_ => {}, () => {
                // 1フレーム後のUpdate後にトリガーをリセットする
                if (self != null) {
                    self.ResetTrigger(id);
                }
            });
    }
}

使う側はこんな感じです。

_animator.SetTriggerOneFrame("Jump");

このくらいまでやっておけば使いやすそうです。

関連

light11.hatenadiary.com