Unityを触り始めた頃、「とりあえず Update() に全部書いておけば動くし楽!」となりがちですよね。
プレイヤーの移動、攻撃、被ダメージ判定、UI更新…すべてを1つの巨大スクリプトに詰め込んでしまうと、次のような問題がすぐに出てきます。

  • どこを直せばいいか分からない(バグ調査がつらい)
  • 機能追加のたびに1ファイルがどんどん肥大化する
  • 敵やギミックにも流用しづらく、コピペ地獄になる

そこでおすすめなのが、「攻撃判定」「被ダメージ判定」「体力管理」などを、それぞれ小さなコンポーネントに分割していく設計です。
この記事では、その中でも「被ダメージ判定」に絞った HurtboxComponent を作り、敵の攻撃(Hitbox)と重なったときに HealthManager のHPを減らす仕組みをコンポーネントとして切り出してみます。

【Unity】当たり判定をコンポーネント化!「HurtboxComponent」コンポーネント

ここでは 2D アクションゲームを想定し、トリガー付きの2Dコライダー(Area的な領域) を「Hurtbox」として扱います。
敵側は「Hitbox」コライダーを持っており、両者が重なった瞬間にダメージを受ける、という構造ですね。

ポイントは次の3つです。

  • HealthManager は「体力管理」だけを担当するシンプルなコンポーネント
  • HurtboxComponent は「何と当たったらどれだけ減らすか」だけを担当
  • 敵側の「Hitbox」はタグや専用コンポーネントで識別し、柔軟に拡張できるようにする

それでは、コピペで動く完全なコードを順番に見ていきましょう。


HealthManager(体力管理)コンポーネント

using UnityEngine;

namespace Sample
{
    /// <summary>
    /// シンプルなHP管理コンポーネント
    /// HurtboxComponent からダメージを受け取る想定
    /// </summary>
    public class HealthManager : MonoBehaviour
    {
        [Header("HP設定")]
        [SerializeField] private int maxHealth = 100;   // 最大HP
        [SerializeField] private int currentHealth = 100; // 現在HP(インスペクタで調整可)

        [Header("死亡時にオブジェクトを破壊するか")]
        [SerializeField] private bool destroyOnDeath = true;

        /// <summary>現在のHP(読み取り専用)</summary>
        public int CurrentHealth => currentHealth;

        private void Awake()
        {
            // currentHealth が 0 なら maxHealth で初期化しておく
            if (currentHealth <= 0)
            {
                currentHealth = maxHealth;
            }
        }

        /// <summary>
        /// ダメージを受ける(負の値は回復として扱ってもOK)
        /// </summary>
        /// <param name="amount">ダメージ量(正の値で減少)</param>
        public void ApplyDamage(int amount)
        {
            if (amount == 0)
            {
                return;
            }

            currentHealth -= amount;
            currentHealth = Mathf.Clamp(currentHealth, 0, maxHealth);

            Debug.Log($"[{name}] took damage: {amount}, HP: {currentHealth}/{maxHealth}");

            if (currentHealth <= 0)
            {
                OnDeath();
            }
        }

        /// <summary>
        /// 死亡時の処理
        /// ゲームによってはアニメーションやリスポーン処理に差し替える
        /// </summary>
        protected virtual void OnDeath()
        {
            Debug.Log($"[{name}] is dead.");

            if (destroyOnDeath)
            {
                Destroy(gameObject);
            }
        }
    }
}

EnemyHitbox(攻撃判定の情報を持つコンポーネント)

敵の攻撃判定(Hitbox)側に、「この攻撃はいくらダメージを与えるのか?」という情報を持たせるコンポーネントです。
これを付けたオブジェクトの 2D コライダーを「Hitbox」とみなし、Hurtbox と重なったときに参照します。

using UnityEngine;

namespace Sample
{
    /// <summary>
    /// 敵の攻撃判定(Hitbox)に付けるコンポーネント
    /// ダメージ量などを保持する
    /// </summary>
    [RequireComponent(typeof(Collider2D))]
    public class EnemyHitbox : MonoBehaviour
    {
        [Header("ダメージ設定")]
        [SerializeField] private int damage = 10; // このヒットボックスが与えるダメージ

        [Header("一度の接触で一回だけダメージを与えるか")]
        [SerializeField] private bool damageOncePerTouch = true;

        // Hurtbox側で「同じヒットボックスからの連続ダメージ」を制御したい場合に使う
        public int Damage => damage;
        public bool DamageOncePerTouch => damageOncePerTouch;
    }
}

HurtboxComponent(被ダメージ判定)コンポーネント

メインの主役です。
2D のトリガーコライダー(いわゆる Area2D 的な領域)を持ち、敵の Hitbox(EnemyHitbox コンポーネント付きオブジェクト)と重なったら HealthManager にダメージを渡します。

using System.Collections.Generic;
using UnityEngine;

namespace Sample
{
    /// <summary>
    /// 被ダメージ判定を担当するコンポーネント
    /// 2Dトリガーコライダーを「Hurtbox」として扱い、
    /// 敵の Hitbox (EnemyHitbox) と重なったら HealthManager にダメージを送る
    /// </summary>
    [RequireComponent(typeof(Collider2D))]
    public class HurtboxComponent : MonoBehaviour
    {
        [Header("参照コンポーネント")]
        [SerializeField] private HealthManager healthManager; // ダメージを適用する対象

        [Header("Hitbox識別設定")]
        [Tooltip("EnemyHitbox コンポーネントを探す前に、タグでフィルタしたい場合に指定")]
        [SerializeField] private string hitboxTag = "EnemyHitbox";

        [Header("デバッグ設定")]
        [SerializeField] private bool showDebugLog = true;

        // 「一度の接触で一回だけダメージを与える」Hitbox用に、
        // すでにダメージを与えた EnemyHitbox を管理する
        private readonly HashSet<EnemyHitbox> _alreadyDamagedHitboxes = new HashSet<EnemyHitbox>();

        private Collider2D _collider2D;

        private void Reset()
        {
            // コンポーネント追加時に、トリガー設定を自動でONにしておく
            _collider2D = GetComponent<Collider2D>();
            if (_collider2D != null)
            {
                _collider2D.isTrigger = true;
            }

            // 同じオブジェクト上の HealthManager を自動参照
            if (healthManager == null)
            {
                healthManager = GetComponent<HealthManager>();
            }
        }

        private void Awake()
        {
            _collider2D = GetComponent<Collider2D>();
            if (_collider2D != null && !_collider2D.isTrigger)
            {
                Debug.LogWarning($"[{name}] HurtboxComponent の Collider2D は isTrigger = true を推奨します。自動で変更しました。");
                _collider2D.isTrigger = true;
            }

            if (healthManager == null)
            {
                // 同じオブジェクト上に HealthManager があれば自動取得
                healthManager = GetComponent<HealthManager>();
            }

            if (healthManager == null)
            {
                Debug.LogError($"[{name}] HurtboxComponent に HealthManager の参照が設定されていません。");
            }
        }

        private void OnTriggerEnter2D(Collider2D other)
        {
            // タグ指定がある場合は、まずタグでフィルタ
            if (!string.IsNullOrEmpty(hitboxTag) && !other.CompareTag(hitboxTag))
            {
                return;
            }

            // EnemyHitbox コンポーネントを探す
            if (!other.TryGetComponent(out EnemyHitbox enemyHitbox))
            {
                return;
            }

            // すでに一度ダメージを与えていて、かつ
            // 「一度の接触で一回だけダメージ」設定ならスキップ
            if (enemyHitbox.DamageOncePerTouch && _alreadyDamagedHitboxes.Contains(enemyHitbox))
            {
                return;
            }

            ApplyDamageFromHitbox(enemyHitbox);
        }

        private void OnTriggerExit2D(Collider2D other)
        {
            // 接触が終わったら、ダメージ済みリストから削除して
            // 再度触れた時にダメージを与えられるようにする
            if (!other.TryGetComponent(out EnemyHitbox enemyHitbox))
            {
                return;
            }

            if (_alreadyDamagedHitboxes.Contains(enemyHitbox))
            {
                _alreadyDamagedHitboxes.Remove(enemyHitbox);
            }
        }

        /// <summary>
        /// EnemyHitbox からダメージを適用する
        /// </summary>
        /// <param name="enemyHitbox">接触した EnemyHitbox</param>
        private void ApplyDamageFromHitbox(EnemyHitbox enemyHitbox)
        {
            if (healthManager == null)
            {
                Debug.LogWarning($"[{name}] HealthManager が設定されていないため、ダメージを適用できません。");
                return;
            }

            int damage = Mathf.Max(0, enemyHitbox.Damage);
            if (damage <= 0)
            {
                // 0ダメージなら何もしない
                return;
            }

            healthManager.ApplyDamage(damage);

            if (enemyHitbox.DamageOncePerTouch)
            {
                _alreadyDamagedHitboxes.Add(enemyHitbox);
            }

            if (showDebugLog)
            {
                Debug.Log($"[{name}] が {enemyHitbox.name} から {damage} ダメージを受けました。");
            }
        }
    }
}

使い方の手順

ここからは、実際にプレイヤーと敵を例にして、このコンポーネント群をどう使うかを手順で見ていきましょう。

手順① プレイヤーに HealthManager と HurtboxComponent を付ける

  1. シーン内のプレイヤー GameObject を選択します(例: Player)。
  2. Add Component から HealthManager を追加します。
    Max HealthCurrent Health を好きな値(例: 100)に設定します。
  3. 同じく Add Component から HurtboxComponent を追加します。
  4. プレイヤーに Collider2D(例: CircleCollider2DBoxCollider2D)を追加し、Is Trigger にチェックを入れます。
    HurtboxComponent が自動でトリガーにしてくれますが、見た目としてもONにしておくと安心です。
  5. HurtboxComponentHealth Manager フィールドに、同じオブジェクトの HealthManager をドラッグ&ドロップして設定します(または自動で入っていればOK)。
  6. 必要であれば Hitbox Tag に「EnemyHitbox」などのタグ名を設定します(後で敵側に同じタグを付けます)。

手順② 敵に Hitbox(EnemyHitbox)を用意する

  1. シーン内の敵 GameObject を選択します(例: Enemy)。
  2. 敵の「攻撃判定」となる子オブジェクトを作成します(例: EnemyHitboxArea)。
    Enemy を右クリック → Create Empty → 名前を EnemyHitboxArea に変更。
  3. EnemyHitboxAreaCollider2D を追加し、Is Trigger にチェックを入れます。
    ・プレイヤーに当たる範囲に合わせてサイズを調整します。
  4. EnemyHitboxAreaEnemyHitbox コンポーネントを追加し、Damage を 10 や 20 など任意の値に設定します。
  5. タグによるフィルタを使う場合は、EnemyHitboxAreaEnemyHitbox というタグを作成・設定します。
    ・Unity 上部メニューの Tags & Layers から新しいタグを作り、EnemyHitboxArea に割り当ててください。

手順③ プレイヤーと敵を配置してテストする

  1. シーン内でプレイヤーと敵の位置を近づけ、敵の Hitbox(EnemyHitboxArea のコライダー)がプレイヤーの Hurtbox(プレイヤーの 2D コライダー)と重なるように配置します。
  2. ゲームを再生し、コンソールログを確認します。
    ・接触した瞬間に [Player] took damage: 10, HP: 90/100 のようなログが出れば成功です。
  3. 敵側の EnemyHitboxDamage Once Per Touch を ON にしている場合、
    ・1回触れたあと、そのまま触れ続けてもダメージが増えないことを確認してみましょう。

手順④ 応用例:動く床やトラップにも Hurtbox を使う

この仕組みはプレイヤー専用ではありません。
例えば、次のようなオブジェクトにも HurtboxComponent を付けるだけで、同じロジックを再利用できます。

  • 動く床:床に HP を持たせて、敵の攻撃で壊れるギミック
  • 防御壁:壁にも HP を持たせて、一定ダメージで破壊
  • 味方NPC:プレイヤーと同じ HealthManager + HurtboxComponent 構成で被ダメージを共有

どのオブジェクトも「HP管理」と「被ダメージ判定」のコンポーネントを付けるだけで済むので、プレハブ化して量産するのがとても楽になります。


メリットと応用

コンポーネント分割のメリット

  • 責務が明確HealthManager は「HPの増減と死亡処理」だけ、HurtboxComponent は「何と当たったらダメージを渡すか」だけを担当します。
  • 再利用性が高い:プレイヤー、敵、ギミックなど、どのオブジェクトにも同じコンポーネントを付けるだけで被ダメージ機能を共有できます。
  • プレハブ管理が楽:HPやダメージ量はインスペクタから変更できるため、レベルデザイナーがコードを触らずにバランス調整できます。
  • テストしやすい:HPだけをテストしたいときは HealthManager だけを見る、当たり判定だけを確認したいときは HurtboxComponent だけを見る、という切り分けができます。

改造案:無敵時間(i-frames)を追加する

よくある拡張として、「ダメージを受けた直後は一定時間ダメージを受け付けない(無敵時間)」という仕様があります。
これは HurtboxComponent に少しコードを足すだけで実現できます。

例として、HurtboxComponent に次のようなメソッドとフィールドを追加してみましょう。

        [Header("無敵時間設定")]
        [SerializeField] private float invincibleDuration = 0.5f; // ダメージ後の無敵時間(秒)

        private float _invincibleUntilTime = 0f;

        /// <summary>
        /// 現在無敵かどうか
        /// </summary>
        private bool IsInvincible()
        {
            return Time.time < _invincibleUntilTime;
        }

        // ApplyDamageFromHitbox の先頭に以下を追加
        private void ApplyDamageFromHitbox(EnemyHitbox enemyHitbox)
        {
            if (IsInvincible())
            {
                // 無敵時間中はダメージを受けない
                return;
            }

            // ここから既存処理...
            // healthManager.ApplyDamage(damage); の直後に
            _invincibleUntilTime = Time.time + invincibleDuration;
        }

これにより、「連続ヒットで一瞬でHPが溶ける」問題を簡単に防げます。
さらに、無敵中はスプライトの点滅やシェーダー変更を行うコンポーネントを別途用意すれば、視覚的なフィードバックもきれいに分離して実装できます。

このように、小さなコンポーネントに責務を分けておくと、後からの機能追加や改造がとてもやりやすくなります。
巨大な Update() にすべてを書き込む前に、「これは1つのコンポーネントに切り出せないかな?」と考えるクセをつけていきましょう。