Skip to content

第04回 衝突判定とダメージ ​

前回: 第03回 攻撃入力と攻撃モーション / 次回: 第05回 Enemyの探索・追跡・攻撃

今回の授業内容 ​

  • Triggerによる接触イベントの発生条件を理解する
  • 入力、攻撃時間、命中判定、ダメージ解決、HP管理を分けて実装する
  • 攻撃中だけTrigger Colliderを有効にする
  • 同じ攻撃が同じ相手へ複数回当たらないようにする
  • 攻撃からHP減少までの処理の流れを確認する
  • HPゲージへダメージ結果を反映する

今回の配布コード ​

第03回までの移動、カメラ、攻撃モーションを含み、第04回終了時点の全スクリプトを収録しています。今回追加するダメージ処理と、前回使った攻撃演出を同じ回のフォルダから確認できます。HealthGauge.csはDOTweenを使用するため、この回の「DOTweenを導入する」を先に完了してください。

ファイル内容
PlayerController.cs移動、カメラ、攻撃の入力
PlayerMove.csプレイヤーの移動と振り向き
CameraMove.csカメラの追従と回転
Health.csHPの保持、ダメージ、回復、死亡判定
DamageReceiver.csダメージを受け取る共通コンポーネント
AttackContext.cs攻撃者とダメージ量の情報
AttackResolver.cs攻撃ルールの判定とダメージ適用
CharacterAttack.cs攻撃時間と命中判定の管理
Player.csPlayerのHPと表示の接続
Enemy.csEnemyのHPと死亡処理
AttackEffect.cs剣を表示して振る攻撃演出
SlashEffect.cs剣閃エフェクトの制御
HealthGauge.csHPゲージの表示

小さく確認してから接続する ​

この回の中心作業は接触の検出、Healthの値の変化、命中済み対象の記録です。AttackContext・AttackResolver・HPゲージの宣言と接続は配布コードを読みながら利用し、最初から全ファイルを書き直す必要はありません。

段階最小の確認次へ進む条件
接触OnTriggerEnterでother.nameをログへ出す指定した相手の名前が出る
HPHealthへ3のダメージを渡す初期値10が7になり、10以上のダメージで0に止まる
重複防止同じ攻撃中に同じ相手へ再接触するHPがその攻撃で1回だけ減る
表示ダメージ通知を購読するログで通知を確かめてからHPゲージへつなぐ

ゲージの演出を調整する前に数値を確認します。最終的な責務分担はそのまま使い、各部品が期待どおり動くかを順に調べます。

プレイヤーの攻撃と敵のHP減少 ​

Playerの攻撃入力からEnemyのHP減少までを、次の流れで実装します。

攻撃入力からHP減少までの流れ
Player
攻撃ボタンの入力を伝える
Player Attack
Player Attack
攻撃中のColliderで接触を検出する
Enemy
Enemy
ダメージを受け取る
DamageReceiver
DamageReceiver
HPを減らす
Health

攻撃とHP管理を責務ごとに分ける ​

攻撃処理をCharacterAttackと名付ける ​

この時点ではPlayerからだけ攻撃を呼び出しますが、攻撃時間、攻撃Collider、命中済み対象、ダメージ解決はPlayer入力そのものには依存しません。次回はEnemyも同じ仕組みで攻撃するため、Player専用のPlayerAttackではなく、最初からCharacterAttackという共通名で実装します。

共通の攻撃処理を利用するコンポーネント
PlayerController
第03回の接続を利用する
CharacterAttack
EnemyController
第05回で接続する
CharacterAttack
CharacterAttack
命中後のルールを任せる
AttackResolver

第03回のPlayerControllerからの呼び出しを使い、今回は命中判定とダメージ処理を加えます。第05回ではEnemyControllerからも利用します。この回ではEnemyの追跡や攻撃判断は扱いません。

命中後のルールを別の部品へ任せる ​

例えば小規模なゲームなら、CharacterAttack の OnTriggerEnter でEnemyの Health を直接減らす実装でも問題はありません。ですが一定規模以上のゲームでは、開発が進むにつれて CharacterAttack にゲームのルールや特殊な条件判定を書き連ねていくことになりかねません。

壊せるObject、無敵状態、防御力、属性などの仕様が増えるたび、CharacterAttack の中に分岐が増え続けるのは問題です。攻撃命中の処理と、その後何が起きるかの処理は分けるべきでしょう。

保守性を確保するためには、例えば CharacterAttack でダメージ計算まで扱うのではなく、ゲームの仕様に関する処理は AttackResolver に任せるような構成が考えられます。どのコンポーネントが何を扱い、それ以外を扱わないかを徹底することで、コードの可読性とメンテナンス性を高めることができます。

攻撃とHP管理の責務
  • PlayerController
    • 入力を扱い、攻撃の指示を出す
  • CharacterAttack
    • 攻撃の有効時間やタイミング、
    • 攻撃判定のColliderを管理する
  • AttackResolver
    • 攻撃を評価して適切に振り分ける
  • DamageReceiver
    • ダメージを受ける処理を直接担当
  • Health
    • HPの値とその増減を扱う

Colliderでゲーム中の接触を検出する ​

  • 攻撃範囲に敵が入った
  • 弾がキャラクターに命中した
  • アイテムに触れて取得した
  • ダメージ床に入ってHPが減った

これらの動作はすべて接触をきっかけに起こります。ゲームでインタラクションを作るためには、なんらかの当たり範囲同士が重なったときに検知する仕組みが必要になります。Unityでは Collider と物理エンジンを使って接触を検出できます。

物体の当たり範囲を表すCollider ​

Collider は、GameObjectの当たり範囲を表すComponentです。Box Collider、Sphere Collider、Capsule Collider など、用途に応じた形状のColliderを使い分けます。

ゲームでは、当たり判定の形状や範囲を見た目の形状と完全に一致させる必要はありません。人間の感覚と幾何学的な形状は必ずしも一致しないため、当たり判定は見た目よりも操作感を優先して設定するのが基本となります。多くの場合、プレイ感を優先してシンプルな形状の Collider が使用されます。

CollisionとTriggerを使い分ける ​

通常のColliderは、物理エンジンによって衝突が発生し、オブジェクト同士が押し合うような反応をします。攻撃判定やアイテム取得のように、衝突は検出したいが物理的な反応は不要な場合は、Colliderの Is Trigger を有効にしてTriggerとして扱います。

種類物理的な反応主な用途
Collisionオブジェクト同士が押し合う壁、床、障害物
Trigger通過を許し、重なりを検出する攻撃範囲、アイテム、ダメージ床

今回、攻撃判定ではTriggerを使って、攻撃範囲に敵が入った瞬間を検出します。

TriggerイベントにはRigidbodyが必要 ​

Collider同士が重なるだけでは Triggerイベントは発生しません。接触はUnityの物理エンジンが判定するため、少なくとも片方のObjectに Rigidbody が必要です。今回はPlayer側に Rigidbody を設定します。Playerはスクリプトで移動させるため Is Kinematic を有効にします。

Player AttackのHierarchy
  • Player
    • Rigidbody
    • Collider
    • Player Attack
      • Collider(Is Trigger)

OnTriggerEnterでコライダーの接触を得る ​

OnTriggerEnter は、別のColliderがTriggerに入った瞬間にUnityから呼ばれるコールバック関数です。引数の other には Triggerに入ってきたColliderが入ります。

cs
// 他のコライダーがTriggerに入った瞬間に呼ばれる
private void OnTriggerEnter(Collider other)
{
    Debug.Log(other.name);
}

範囲内にいるコライダーを捕捉し続ける場合は OnTriggerStay を。コライダーが範囲から出た瞬間を扱う場合は OnTriggerExit を使います。

PlayerとEnemyのオブジェクト構成 ​

Player と Enemy の双方に DamageReceiver を設定して、ダメージの受け取りを可能にします。Player の攻撃判定用の CharacterAttack は、Player本体とは別の子Objectとして作成し、攻撃中だけ有効になるTrigger Colliderを持たせます。

PlayerとEnemyのHierarchy
  • Player
    • Rigidbody
    • Collider
    • PlayerController
    • PlayerMove
    • DamageReceiver
    • Player Attack
      • Attack Collider(Is Trigger)
  • Enemy
    • Collider
    • DamageReceiver
    • Enemy

HP計算をMonoBehaviourから分離する ​

HPの増減や死亡判定は、Scene上の位置やColliderとは関係ありません。GameObjectへ直接追加する必要がない計算処理は、普通のC#クラスとしてMonoBehaviourから分けることができます。

例えば HP計算を扱う Health クラスはダメージを受けた際の HPの増減や死亡判定を担当しますが、MonoBehaviourを継承する必要はありません。今回の実装はInspector表示に使うSerializeFieldと値の制限に使うMathfには依存しますが、GameObjectのライフサイクルから計算処理を分離できます。

HealthでHPの値を管理する ​

Health は HP計算を担当する C#クラスで MonoBehaviour を利用しません。こうした単純なデータ管理クラスをComponentから分けることで、HPの増減や死亡判定のロジックをシンプルに保ちます。

cs
// MonoBehaviourから分離したHP計算クラス
[System.Serializable]
public class Health
{
    [SerializeField] private int maxHealth = 10;

    public int CurrentHealth { get; private set; }
    public int MaxHealth => maxHealth;
    public bool IsDead => CurrentHealth <= 0;

    public void Reset()
    {
        CurrentHealth = maxHealth;
    }
}

TakeDamageとHealにHPの変更処理を集中させる ​

Health は、HPをどの範囲で増減させるかを担当します。0以下のダメージや回復量は無視し、死亡後に追撃が入っても二重に死亡したり生き返ったりしないようブロックします。HP の数値を直接操作させず、TakeDamage() や Heal() を通し集約することで、増減のルールを一元管理できます。

cs
public void TakeDamage(int damage)
{
    if (damage <= 0 || IsDead)
        return;

    CurrentHealth =
        Mathf.Max(CurrentHealth - damage, 0);
}

public void Heal(int heal)
{
    if (heal <= 0 || IsDead)
        return;

    CurrentHealth =
        Mathf.Min(CurrentHealth + heal, maxHealth);
}

イベントで結果を受け取る ​

イベントは、結果に反応するメソッドをあらかじめ登録しておく通知の入口です。DamageReceiverはHPが変わったことを通知し、表示側は登録したメソッドで受け取ります。通知する側がHPゲージの具体的な表示方法を知る必要はありません。

Action<int, int>はintを2つ受け取り、値を返さないメソッドを登録する型です。使用するファイルにはusing System;が必要です。eventを付けると、外部からは登録と解除だけができ、通知の発行は宣言したクラスが担当します。

cs
public event Action<int, int> Damaged;
// DamageReceiverの中で、HPが変わった後に呼ぶ。
Damaged?.Invoke(previousHealth - CurrentHealth, MaxHealth);

このイベントの第1引数は実際に減ったHP、第2引数は最大HPです。残りHPではありません。HPが10から7へ減り最大HPが10なら、受け取る値は(3, 10)です。?.Invokeは登録されたメソッドがある場合だけ呼び出します。

登録・通知・解除をログで確認する ​

DamageReceiverを実装した後、同じGameObjectへ次の確認用コンポーネントを追加します。ログを確認したら外し、配布コードのPlayerによるHP表示へ進みます。

cs
using UnityEngine;

[RequireComponent(typeof(DamageReceiver))]
public class DamageLog : MonoBehaviour
{
    private DamageReceiver receiver;

    private void OnEnable()
    {
        receiver = GetComponent<DamageReceiver>();
        receiver.Damaged += ShowDamage;
    }

    private void OnDisable()
    {
        receiver.Damaged -= ShowDamage;
    }

    private void ShowDamage(int damage, int maxHealth)
    {
        Debug.Log($"減少量={damage}, 最大HP={maxHealth}");
    }
}

+=で登録することを購読、-=で外すことを購読解除と呼びます。通知が発行されると、登録したShowDamageが2つの値を受け取ります。DamageLogを無効にしてからもう一度ダメージを与えるとログが出なくなること、再度有効にしても1回の通知でログが増殖しないことを確認します。

第08回ではこの仕組みを使い、複数Enemyの死亡をゲーム全体へ通知するstaticイベントへ進みます。

DamageReceiverからHealthへ計算を委譲する ​

HPを持たせたいObjectには DamageReceiver を Componentとして追加します。DamageReceiver はHPの増減の対象となるかを扱うコンポーネントです。攻撃の対象となるか否かを管理することで、攻撃者は都度相手がEnemyであるかなどのチェックを行わずにダメージを与えられます。

cs
public class DamageReceiver : MonoBehaviour
{
    [SerializeField] private Health health = new();

    public event Action<int, int> Damaged;
    public event Action<int, int> Healed;

    public bool CanReceiveDamage => !health.IsDead;
    public bool IsDead => health.IsDead;
    public int CurrentHealth => health.CurrentHealth;
    public int MaxHealth => health.MaxHealth;

    private void Awake()
    {
        health.Reset();
    }

    public void ReceiveDamage(int damage)
    {
        var previousHealth = CurrentHealth;

        health.TakeDamage(damage);

        if (CurrentHealth == previousHealth)
            return;

        Damaged?.Invoke(
            previousHealth - CurrentHealth,
            MaxHealth);
    }
}

DamageReceiver は、外部からダメージを受け取る処理の共通コンポーネントです。HP計算は Health に任せ、変化があったときだけ Damaged イベントで通知、演出などを起動します。

AttackResolverで攻撃ルールを適用 ​

AttackResolver は、攻撃が命中した際に具体的に何が起きるかのルールをまとめるクラスです。攻撃結果の判断を集めることで、将来の仕様の変更や拡張に対応しやすくなります。

例えば、攻撃の属性や防御力の計算などの仕様が増えた際もAttackResolver に処理を追加するだけで済み、間接攻撃や全体攻撃といった特殊な攻撃ルールの追加も容易になります。

cs
public static class AttackResolver
{
    public static void Resolve(
        AttackContext attackContext,
        DamageReceiver receiver)
    {
        if (receiver.transform ==
              attackContext.Attacker.transform)
            return;

        if (receiver.transform.IsChildOf(
              attackContext.Attacker.transform))
            return;

        if (!receiver.CanReceiveDamage)
            return;

        receiver.ReceiveDamage(attackContext.Damage);
    }
}

AttackContextへ攻撃情報をまとめる ​

CharacterAttack は、攻撃した自分の情報と与えるダメージ量を AttackContext にまとめて AttackResolver に渡します。後に攻撃の属性や特殊効果などを追加する際も、AttackContext に情報を追加していく形で対応できます。ダメージ処理に必要な情報が集約されるため、仕様の複雑化への防波堤としての役割も期待できます。

cs
public readonly struct AttackContext
{
    public AttackContext(GameObject attacker, int damage)
    {
        Attacker = attacker;
        Damage = damage;
    }

    public GameObject Attacker { get; }
    public int Damage { get; }
}

攻撃者自身を対象から除外する ​

Player自身に DamageReceiver があると自分の攻撃が自分には当たりかねません。CharacterAttack で除外しても構いませんが、ここでは AttackResolver に対応を任せています。

cs
// 攻撃者と被弾した相手が同じ場合は処理をキャンセルする
if (receiver.transform == attackContext.Attacker.transform ||
    receiver.transform.IsChildOf(attackContext.Attacker.transform))
    return;

設計の選択肢

この場所が適切かは一考の余地があります。ゲームの仕様と照らして、どこでフィルターするのが適切かを都度判断しましょう。

PlayerControllerから攻撃を指示する ​

PlayerController は入力を読み取り、移動や攻撃の担当Componentへ処理を渡します。ここでは攻撃ボタンが押された瞬間に CharacterAttack.Attack() を呼んでいます。

攻撃の時間管理や当たり判定は CharacterAttack の担当です。攻撃不可能な場合もあるかもしれませんが、その判定も CharacterAttack の仕事になります。

cs
public class PlayerController : MonoBehaviour
{
    [SerializeField] private PlayerMove playerMove;
    [SerializeField] private CameraMove cameraMove;
    [SerializeField] private CharacterAttack characterAttack;

    private InputSystem_Actions _inputActions;

    private void Awake()
    {
        _inputActions = new InputSystem_Actions();
    }

    private void Update()
    {
        // 中略

        // 攻撃の発動
        if (_inputActions.Player.Attack.WasPressedThisFrame())
            characterAttack.Attack();
    }
}

Player Attackに攻撃判定を設定する ​

Player Attack の Inspector 設定Player Attack の Inspector 設定

  1. Playerの子Objectとして Player Attack オブジェクトを作ります
  2. Player Attack に CharacterAttack コンポーネントを追加します
  3. Capsule Collider などを追加して Is Trigger を有効にします
  4. CharacterAttack の attackCollider に コライダーへの参照を設定して準備完了です

CharacterAttackで攻撃時間を管理する ​

CharacterAttack は、プレイヤーの攻撃に関する処理を担当します。まず、攻撃可能な時間と、攻撃判定が有効な時間を管理します。

一度の攻撃が同じ相手に複数回ヒットしないよう、命中した相手の記録も行います。攻撃のエフェクトの再生リクエストもここで行います。

cs
public class CharacterAttack : MonoBehaviour
{
    [SerializeField] private AttackEffect attackEffect;
    [SerializeField] private Collider attackCollider;
    [SerializeField] private int damage = 1;
    [SerializeField] private float attackDuration = 0.5f;
    [SerializeField] private float hitStartTime = 0.15f;
    [SerializeField] private float hitEndTime = 0.3f;

    private readonly List<DamageReceiver> _hitReceivers = new();
    private bool _isAttacking;
    private bool _isHitActive;
    private float _attackElapsedTime;
}

Attackで攻撃を開始する ​

cs
public class CharacterAttack : MonoBehaviour
{
    // 中略

    public void Attack()
    {
        attackEffect?.Play();

        _hitReceivers.Clear();
        _isAttacking = true;
        _isHitActive = false;
        _attackElapsedTime = 0f;
    }
}

Attack() は攻撃開始用の関数です。今回は PlayerController から呼ばれています。

この時点ではまだColliderを有効にせず、Update で判定開始タイミングをチェックします。攻撃演出があれば再生し、前回命中した相手の記録を削除するなど攻撃に必要な初期化をまとめて行います。

Updateで判定の開始と終了を制御する ​

ゲームでの攻撃は、即時有効とは限りません。攻撃判定発生までの遅延時間、攻撃判定の有効時間、次の攻撃までのクールダウン時間などの時間の管理が必要です。これらの管理は、攻撃の開始と終了を制御する CharacterAttack の仕事になります。

cs
public class CharacterAttack : MonoBehaviour
{
    // 中略

    private void Update()
    {
        if (!_isAttacking)
            return;

        _attackElapsedTime += Time.deltaTime;

        if (!_isHitActive && _attackElapsedTime >= hitStartTime)
            StartHit();

        if (_isHitActive && _attackElapsedTime >= hitEndTime)
            EndHit();

        if (_attackElapsedTime >= attackDuration)
            EndAttack();
    }

Collider.enabledで判定を切り替える ​

attackCollider は攻撃判定用のTrigger Colliderです。攻撃していない時間はColliderが無効なので、ダメージ判定は発生しません。迂遠な処理に見えますが、これで攻撃の演出と実際に当たるタイミングを分けて調整できます。

cs
public class CharacterAttack : MonoBehaviour
{
    // 中略

    private void Awake()
    {
        attackCollider.enabled = false;
    }

    private void StartHit()
    {
        _isHitActive = true;
        attackCollider.enabled = true;
    }

    private void EndHit()
    {
        _isHitActive = false;
        attackCollider.enabled = false;
    }

OnTriggerEnterでDamageReceiverを探す ​

cs
public class CharacterAttack : MonoBehaviour
{
    // 中略

    private void OnTriggerEnter(Collider other)
    {
        var receiver =
            other.GetComponentInParent<DamageReceiver>();

        if (receiver == null)
            return;

        if (_hitReceivers.Contains(receiver))
            return;

        _hitReceivers.Add(receiver);

        var attackContext =
            new AttackContext(gameObject, damage);

        AttackResolver.Resolve(attackContext, receiver);
    }

OnTriggerEnter は、攻撃判定Colliderに他のコライダーが入った瞬間に呼ばれます。今回はダメージを与える相手か否かを DamageReceiver を持っているかで判定しています。見つかった場合は AttackResolver に渡し、実際にダメージを与えるかどうかの判断を任せます。

命中済みの相手を記録する ​

cs
private readonly List<DamageReceiver> _hitReceivers = new();

敵が複数のColliderを持つ場合や激しく移動する場合、1回の攻撃で複数回の命中が発生しかねません。そこで1回の攻撃中に命中した相手をListに記録して、すでに記録されている相手には同じ攻撃で再度ダメージを与えないようにします。

発展

実はListでの管理はあまり効率の良いやり方ではありません。他にも、攻撃の開始と終了でColliderを有効にする方法や、攻撃のIDを付与して管理する方法などがあります。より効率的な方法を考えてみてください。

DOTweenを導入する ​

DOTweenは、値や位置などを時間をかけて変化させるTween処理を簡潔に記述するためのライブラリです。今回はHPゲージの変化と被弾時の揺れに使用します。

  1. Unity Asset Storeで無料版のDOTween (HOTween v2)をマイアセットへ追加します
  2. UnityのPackage Managerを開き、Packages: My AssetsからDOTweenをImportします
  3. Tools > Demigiant > DOTween Utility Panelを開きます
  4. Setup DOTween...を押して初期設定を完了します
  5. ConsoleにDOTween関連のコンパイルエラーがないことを確認します

DOTweenを更新した場合も、Utility PanelからSetup DOTween...を再実行してください。詳しい導入方法はDOTween公式 Get Startedで確認できます。

HealthGaugeでHPを表示する ​

Enemy の HP ゲージEnemy の HP ゲージ

HealthGaugeは、DamageReceiverで変化したHPをUIへ反映します。FrontとBackの2枚のImageを使い、DOTweenでゲージの変化と揺れを再生します。

  1. Canvasの子にHPゲージ用のGameObjectを作ります
  2. FrontとBackのImageを作り、Image TypeをFilledにします
  3. 親ObjectへHealthGaugeを追加します
  4. frontとbackへ対応するImageを割り当てます
  5. Player用とEnemy用のHPゲージを用意します

攻撃確認用のEnemyとPlayerを作る ​

今回は移動も攻撃もしない、攻撃テスト用の敵を作ります。ダメージの受け取りはDamageReceiverに任せ、EnemyはHPゲージの更新とHPが0になった後の破棄を担当します。

EnemyはDamageReceiver.DamagedとHealedを購読し、変化量をHealthGaugeへ渡します。配布版のEnemy.csを使用し、InspectorでhealthGaugeを割り当ててください。

今後の拡張に備えてPlayerも追加します。PlayerはPlayer用のHPゲージを更新し、次回から敵の攻撃を受け取れるようにします。配布版のPlayer.csを使用し、InspectorでhealthGaugeを割り当ててください。

攻撃からHP減少までを確認する ​

  1. プレイヤーの攻撃入力 →
  2. 攻撃の発生と命中範囲の有効化 →
  3. ヒットが判定されたら 攻撃の情報をAttackResolverに渡す →
  4. AttackResolverがダメージを 受け取るべき相手を判断 →
  5. DamageReceiverがHealthへダメージを反映 →
  6. DamagedイベントをPlayerまたはEnemyが受け取り、HealthGaugeを更新

図を準備しています…

Scene上でコンポーネントを接続する ​

Playerを設定する ​

  1. Playerに PlayerController を追加します
  2. Playerの子Objectとして Player Attack を作ります
  3. Player Attack に CharacterAttack を追加します
  4. Player Attack にColliderを追加します
  5. Player Attack のColliderで Is Trigger を有効にします
  6. CharacterAttack の attackCollider に Player Attack のColliderを割り当てます
  7. PlayerにPlayerとDamageReceiverを追加します
  8. Player.healthGaugeにPlayer用のHPゲージを割り当てます

Enemyを設定する ​

  1. Enemy用のCubeやCapsuleを配置します
  2. EnemyにColliderが付いていることを確認します
  3. Enemyに DamageReceiver を追加します
  4. Enemyに Enemy を追加します
  5. Enemy.healthGaugeにEnemy用のHPゲージを割り当てます
  6. PlayerまたはEnemyのどちらかにRigidbodyがあることを確認します

Enemy本体のColliderをTriggerにする必要はありません。

実習:Playerの攻撃でEnemyへダメージを与える ​

  1. DOTweenを導入してSetupを完了します
  2. 配布済みのHealth、DamageReceiver、AttackContext、AttackResolverを確認し、HPの計算と通知を小さな数値で試します
  3. 配布済みのHealthGaugeを用い、Player用・Enemy用のHPゲージを接続します
  4. CharacterAttackコンポーネントをPlayer Attackに追加します
  5. Enemyコンポーネントを攻撃テスト用Enemyに追加します
  6. PlayerのPlayerControllerからCharacterAttack.Attack()を呼びます
  7. Player AttackのColliderをTriggerにし、attackColliderに割り当てます
  8. PlayerとEnemyへHealthGaugeを割り当てます
  9. 攻撃ボタンを押したときだけEnemyのHPとHPゲージが減ることを確認します
  10. EnemyのHPが0になったら削除されることを確認します

攻撃演出と命中のタイミングの比較 ​

Player の攻撃エフェクトPlayer の攻撃エフェクト

第03回の AttackEffect と SlashEffect を使い、剣が振られる時点とEnemyのHPが減る時点を比べてください。コンポーネントを重複して追加する必要はありません。

剣が見えていても、攻撃判定が有効な時間に相手へ接触していなければダメージは入りません。演出、判定の有効時間、接触、HPの変化の順で確認し、見た目だけで命中を判断しないようにします。

動作しない場合のチェックリスト ​

  • 攻撃ボタンで CharacterAttack.Attack() が呼ばれているか
  • CharacterAttack.attackCollider にColliderが割り当てられているか
  • 攻撃判定Colliderの Is Trigger が有効か
  • 攻撃中に attackCollider.enabled が true になるか
  • EnemyにColliderが付いているか
  • Enemyに DamageReceiver が付いているか
  • PlayerとEnemyのhealthGaugeが割り当て済みか
  • DOTweenのSetupが完了しているか
  • PlayerまたはEnemyのどちらかにRigidbodyが付いているか
  • Layer Collision Matrixで接触が無効になっていないか

完成条件 ​

  • 攻撃入力をしたときだけ攻撃判定が有効になる
  • 1回の攻撃で同じEnemyのHPが1回だけ減る
  • PlayerとEnemyのHPゲージがHPの変化を反映する
  • HPが0になったEnemyが削除される
  • CharacterAttack、AttackResolver、DamageReceiver、Healthの役割を説明できる

まとめ ​

  • Playerは入力を受け取り、CharacterAttackへ攻撃開始を指示する
  • CharacterAttackは攻撃中だけTrigger Colliderを有効にする
  • OnTriggerEnterで接触した相手のDamageReceiverを探す
  • AttackResolverは攻撃ルールを判断してダメージを適用する
  • DamageReceiverはダメージを受け取り、HP計算をHealthへ任せる
  • HPゲージはDamageReceiverのイベントから更新する
  • 攻撃演出は基本の命中処理から分離する