ゴマちゃんフロンティア

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

【Unity】StateMachineBehaviourからアニメーションの特定タイミングで処理を実行する方法

time 2016/11/16

というわけで、ひさびさにUnity関係のお話です!
今回はタイトルにもある「StateMachineBehaviour」を使ったスクリプトのお話です。

shot2ss20161116195302218

「StateMachineBehaviour」を継承したクラスは、AnimatorController のステートに設定することができます。
主に「ステートに遷移した直後」「ステート中」「他のステートへ遷移する直前」のタイミングで関数が呼び出されますが、「ある特定のタイミングで関数を呼び出す」ということが出来ません。

アニメーションをインポートした際に ImportSettings の Events から出来るようですが、指定する関数名を直打ちするのがあまり好きではありません。
SendMessage() もそうですが、エディタのリファクタリング機能で置換できないので、後々変更があった場合に面倒なことになってしまいます。
アクセス修飾子が private であってもぶち抜いて実行されるのもどうかと思います。

特定タイミングで処理を実行するスクリプトの作成

冒頭で「Events をつかいたくない!」と言ったものの、現状の StateMachineBehaviour に「アニメーションの特定タイミングで処理を実行する」的な機能はありません。
なので自作するわけですが、ステート中に実行される OnStateUpdate() と 、AnimatorStateInfo.normalizedTime を組み合わせればいけそうです。

そんなわけで作ったスクリプトがこちら!

using UnityEngine;

namespace StateController.PlayerAnimator {

    public class ExecuteTest : StateMachineBehaviour {

        [SerializeField]
        protected float execute_time;

        private bool execute_flg;

        public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            execute_flg = false;
        }

        public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            if (execute_time < stateInfo.normalizedTime && !execute_flg) {
                execute_flg = true;

                // 何らかの処理
                Debug.Log("hogehoge");
            }
        }

        public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            execute_flg = false;
        }
    }
}

ポイントは OnStateEnter() と OnStateExit() 内で execute_flg をfalseに設定している部分です。
これがないと execute_flg が true から切り替わらず、2回目以降if文の中が実行されません。
Enter で行えば Exit では要らない気もしますが、念のため!

またif文の条件ですが、normalizedTime が f loat で細かく変化する関係上、「execute_time == stateInfo.normalizedTime」では不可能に近いです。
四捨五入することも考えましたが、近い値になると複数回通ってしまうためNG。

shot2ss20161116202600048

これを AnimatorController 上のステートに設定し、execute_time を設定すればOKです。
AnimatorStateInfo.normalizedTime はアニメーション実行時間に応じて0~1の値を取るので、execute_time も0~1で指定する必要があります。

ただし、ループするステートの場合は上手くいきません。
ループするステートの場合は normalizedTime が1でリセットされず、どんどん加算されていきます。

ということで、normalizedTime 小数点以下を取得して判定するように修正します。
直したのは OnStateUpdate() のif文判定だけです。

using UnityEngine;

namespace StateController.PlayerAnimator {

    public class ExecuteTest : StateMachineBehaviour {

        [SerializeField]
        protected float execute_time;

        private bool execute_flg;

        public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            execute_flg = false;
        }

        public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            if (execute_time < stateInfo.normalizedTime % 1 && !execute_flg) {
                execute_flg = true;

                // 何らかの処理
                Debug.Log("hogehoge");
            }
        }

        public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            execute_flg = false;
        }
    }
}

「剰余演算子」なるものを使えば簡単にできるみたいです。
ちなみに normalizedTime の整数部分がループ回数に相応するようです。
覚えておくと役に立つ・・・かも。

使用例

自分が使っている一例として、「特定タイミングで iTween.MoveBy() を実行しキャラクターを移動させる」があります。
上で載せたスクリプトを少し修正します。

using UnityEngine;

namespace StateController.PlayerAnimator {

    public class MoveCharacter : StateMachineBehaviour {

        [SerializeField]
        protected float execute_time;

        [SerializeField]
        protected Vector3 velocity;

        [SerializeField]
        protected float move_time;

        private bool execute_flg;

        public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            execute_flg = false;
        }

        public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            if (execute_time < stateInfo.normalizedTime % 1 && !execute_flg) {
                execute_flg = true;

                iTween.Stop(animator.gameObject);
                iTween.MoveBy(animator.gameObject, iTween.Hash(
                    "amount", velocity,
                    "time", move_time
                ));
            }
        }

        public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
            execute_flg = false;
        }
    }
}

shot2ss20161116202724703

インスペクター上で実行するタイミングと移動ベクトル、移動時間を設定します。
アニメーションの途中で「1歩踏み込んで攻撃」的なモーションがあっても安心です。

20161116_01

実際に動かすと上のような感じ。
分かりにくいですが、縦斬り→薙ぎ払いまで1モーションで、execute_time は0.5に設定してあります。

別に iTween でなくても良いので、animator.gameObject.getComponent() とかと組み合わせればもっといろいろ出来ます。
StateMachineBehaviour のスクリプト内に処理をあれこれ書きたくない方は、素直に Player クラスあたりを getComponent() して処理させるのも有りだと思います。

まとめ

そんなわけで、StateMachineBehaviour 上から特定タイミングで処理を実行する方法について考えてみました!
特定タイミングであれこれしたい場面は多いので、活用する機会は多くなりそうです。

down

コメントする