ゴマちゃんフロンティア

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

【Unity】被ダメージ時のノックバックをRigidbodyやCharacterControllerで実装する

time 2016/01/10

sync 更新履歴
2020/06/17
記事を全体的に書き直しました。

今回はUnityのキャラクターのノックバックに関するお話です。
ここでの「ノックバック」は「ダメージを受けたキャラクターが後退りする」挙動のことで、アクションゲームなどでよく見かけるものですね。

そんなノックバックを私がどのように実装しているかをご紹介します。
ケースごとにいくつか方法があり、それぞれ書きやすさや挙動が違うので、ゲームに合わせた実装をしてみてください。

ダメージ発生時のメソッド

ダメージ発生時に以下のメソッドが呼ばれることを前提とします。

private bool isInvincible;

public async void Damage()
{
    // 無敵中は処理しない
    if (isInvincible) {
        return;
    }

    isInvincible = true;

    // ここにノックバック処理

    await UniTask.Delay(TimeSpan.FromSeconds(0.5f));

    isInvincible = false;
}

ダメージ発生後は無敵時間のフラグを立て、一定時間は再度ダメージ処理が走らないようにします。フラグはUniTask.Delay()で一定時間待機後に戻します。
「UniTaskよくわからない!」な人はコルーチンやDOTweenのDOVirtual.DelayedCall()などを使ってもOKです。

実際にはこれに加えてアニメーションの再生やマテリアルの切り替えなどを行っていますが、ここでは割愛します。

Rigidbodyで物理演算をさせている場合

この場合はRigidbodyのAddForce()を呼ぶだけ。すごく簡単。

var rigidbody = GetComponent<Rigidbody>();
rigidbody.AddForce(-transform.forward * 5f, ForceMode.VelocityChange);

ノックバックさせる方向はtransform.fowardの逆側にしています。
もし「攻撃を受けた方向」にしたい場合はDamage()の引数にダメージ発生元の座標を渡して計算しましょう。

ForceModeって指定しないとダメだっけ?

デフォルトのForceModeは「Force」で、これは「継続的な力を加える場合」に使います。具体的には移動入力とかですね。
今回のような「瞬間的に力を加える」な場合には適しません。
また今回はノックバックということで、質量を無視するVelocityChangeを指定しています。

ちなみにRigidbodyを使用していてもvelocityに代入して動かしている場合はこの方法だと微妙なので、次節の方法をご参照ください。

CharacterControllerや自動移動なしのNavMeshを使用している場合

この場合はCharacterControllerやNavMeshAgentのMove()を呼び出して動かします。
しかしRigidbodyのAddForce()は1度呼べばあとは物理演算が勝手にやってくれますが、Move()の場合は一定時間呼び出し続ける必要があります。

Update()内で移動させる場合

ダメージ発生時にノックバックの移動速度をフィールドに保存しておき、Update()で処理させる方法です。

private Vector3 knockbackVelocity = Vector3.zero;

private void Update()
    if (knockbackVelocity != Vector3.zero) {
        var characterController = GetComponent<CharacterController>();
        characterController.Move(knockbackVelocity * Time.deltaTime);
    }
}

public async void Damage()
{
    /* 中略 */

    knockbackVelocity = (-transform.forward * 5f);

    await UniTask.Delay(TimeSpan.FromSeconds(0.5f));

    knockbackVelocity = Vector3.zero;
    
    /* 中略 */
}

後述のDOTweenを使う方法と比べてシンプルで、中断する場合もknockbackVelocityVector3.zeroを突っ込むだけなので楽です。
↑のGIFアニメーションだと若干吹っ飛びすぎているので、knockbackVelocityの値とリセットまでの時間を調整してみてください。

サンプルでは説明用に`Update()`内で`GetComponent()`を行っています。
実際に使う場合は`Start()`などで取得するようにしましょう。

DOTweenを使う場合

DOTween.To()を使います。Damage()関数内で完結できるので私はこちらのほうが好みです。

var characterController = GetComponent<CharacterController>();
DOTween.To(
    () => transform.position,
    v => {
        Vector3 velocity = (v - transform.position) * Time.deltaTime;
        characterController.Move(velocity);
    },
    transform.position + (-transform.forward * 5f),
    0.5f
);

第1引数で「どこから」、第2引数のラムダ式で「何に」、第3引数で「どこまで」を指定します。
http://dotween.demigiant.com/documentation.php

「どこから」と「どこまで」はともかく、「何に」がなんだかさっぱり…。

「何に」が初見だと分かりにくいかもしれませんが、引数のvには「第1引数と第3引数の間の座標」が入ります。しかしMove()で指定する値は速度(velocity)なので、vをそのまま渡すとおかしなことになりますね。
なので「現在位置 – v」で現在位置から次の地点までの移動量を取得し、それをMove()の移動に利用します。

また処理落ちすると移動距離が変わってしまうので、Time.deltaTimeを掛けて経過時間の差を吸収します。

ゲームに応じて第3引数で距離を、第4引数でのけぞり時間を調整しましょう。

down

コメントする