ゴマちゃんフロンティア

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

【Unity】「アニメーションの特定タイミングで処理を行う」やり方を変えたお話

time 2021/02/13

今回は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の取得について

StateInfoは`Update()`内で取得しないとだめなの?

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

down

コメントする