2020/11/09
今回はUnityのアニメーションとそれに対するスクリプトなお話です。
アニメーションに合わせて特定の処理をしたいことありますよね。アクションゲームなら「攻撃モーションの途中で攻撃判定を出す」とかですね。
今まではStateMachineBehaviourとAnimationControllerのパラメータを組み合わせて実装していましたが、実装するキャラクター数分設定するのはしんどくなってきます。
またアニメーションが増えてくると、AnimatorControllerのノードも見にくくなって設定がつらくなります。
なのでAnimator側に依存せず、キャラクターのコンポーネント内で完結できるように作り替えてみました。
基本的な考え方
AnimatorのStateInfoの状態を見て、UniTaskで適切にawaitして実現します。
[SerializeField] private Animator animator; private AnimatorStateInfo stateInfo; private bool IsExecute { get; private set; } private void Update() { if (animator) { stateInfo = animator.GetCurrentAnimatorStateInfo(0); } } private async UniTask ExecuteAttack() { if (IsExecute) { return; } IsExecute = true; // モーションを実行(実行後にAnimator更新のため1フレーム待つ) animator.Play("Attack"); await UniTask.DelayFrame(1); var stateInfo = ; // 攻撃判定出現タイミングまで待機 await UniTask.WaitUntil(() => 0.3f <= stateInfo.normalizedTime); // ここで攻撃判定を有効化 // モーション終了まで待機 await UniTask.WaitUntil(() => 1f <= stateInfo.normalizedTime); // 待機モーションを再生 animator.CrossFade("Idle", 0.1f); IsExecute = false; }
ポイントはAnimator.Play()
後に1フレームだけ待機することです。
これを挟まないと攻撃実行前のアニメーションの情報を参照してしまいます。1フレーム待って攻撃モーションに切り替わってから待機判定をしましょう。
取得後はアニメーション上で攻撃判定を出すタイミングまで待機します。剣であれば「剣を振りかぶる直前」とかですね。
UniTask.WaitUntil()
は条件を満たすまで待機します。stateInfo.normalizedTime
がアニメーション全体の長さを1とした時の経過時間になるので、実行したいタイミングまでfalseになるような条件式を書きます。例では「0.3f」なので、アニメーション全体の3割が経過したタイミングまで待機させています。
あとは// ここで攻撃判定を有効化
の部分で攻撃判定用のオブジェクトをInstantiate()
するなりSetActive()
で有効化するなりします。ここはゲーム内容に合わせてどうぞ。
使う際はExecuteAttack()
を呼び出すだけです。
非同期メソッドなので呼び出し側で待機するならawait、しないなら返り値をForget()
しましょう。
StateInfoの取得について
Update()
内での取得は必須ではなく、ExecuteAttack()
内で取得しても問題ありません。しかし書き方に注意する必要があります。
UniTask.WaitUntil()
のラムダ式の外側で取得した場合、その時点の情報から更新されないため正しく待機できません。以下のようにラムダ式の内側で取得して判定しましょう。
await UniTask.WaitUntil(() => { var stateInfo = animator.GetCurrentAnimatorStateInfo(0); return 0.3f <= stateInfo.normalizedTime; });
被ダメージ等で途中で中断する場合
私の場合は「被ダメージ中かどうか」をクラス内で判断できるようにするため、IsDamaged
というプロパティを用意し、被ダメージ時にフラグを切り替えています。
攻撃実行時にそのフラグを確認し、被ダメージ中であればreturnしてしまいます。
private bool IsExecute = false; private bool IsDamaged { get; private set; } private void Update() { if (animator) { stateInfo = animator.GetCurrentAnimatorStateInfo(0); } } private async UniTaskVoid ExecuteAttack() { if (IsExecute) { return; } IsExecute = true; // モーションを実行(実行後にAnimator更新のため1フレーム待つ) animator.Play("Attack"); await UniTask.DelayFrame(1); // 攻撃判定出現タイミングor被ダメまで待機 await UniTask.WaitUntil(() => { return 0.3f <= stateInfo.normalizedTime || IsDamaged; }); // 被ダメだったら処理を止める if (IsDamaged) { IsSkillExecute = false; return; } // ここで攻撃判定を有効化 // モーション終了or被ダメまで待機 await UniTask.WaitUntil(() => { return 1f <= stateInfo.normalizedTime || IsDamaged; }); // 待機モーションを再生 animator.CrossFade("Idle", 0.1f); IsExecute = false; }
もし「フラグを持ちたくない」「中断する原因が複数ある」な場合はCancellationTokenSource
を使うと綺麗に中断できます。
このあたりはUniTaskでぐぐると出てくるので今回は割愛します。
あとがき
ということでアニメーションの特定タイミングで実行する処理を1メソッドにまとめてみました。
これのメリットはAnimatorを使用する側で待機時間やアニメーション名を制御できることですね。攻撃実行メソッドにパラメータで渡して共通化する…みたいなことが容易になりました。
特にキャラクターが多くなるゲームでは、こちらのほうが最終的な負担は軽くなりそうです。