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を使用する側で待機時間やアニメーション名を制御できることですね。攻撃実行メソッドにパラメータで渡して共通化する…みたいなことが容易になりました。
特にキャラクターが多くなるゲームでは、こちらのほうが最終的な負担は軽くなりそうです。





