Unityを触り始めた頃って、つい「とりあえず全部 Update に書いておけば動くし楽!」となりがちですよね。ダメージ処理、HP管理、エフェクト再生、サウンド、UI更新…全部ひとつの Player スクリプトに詰め込んでしまうと、少し仕様を変えたいだけでも地獄のような修正作業になります。

とくに「ダメージを反射する」「一部だけ軽減する」「属性ごとに倍率を変える」といったバトルロジックを Player や Enemy の巨大スクリプトに直書きしてしまうと、再利用しづらい・テストしづらい・バグを埋め込みやすいという三重苦になりがちです。

そこで今回は、「ダメージを受けたとき、その一部を攻撃してきた相手に返す」という機能だけに責務を絞った、コンポーネント指向なスクリプト「DamageReflection」を作ってみましょう。

HP管理コンポーネントなどにフックして使えるようにしておけば、「この敵だけダメージ反射」「このトラップだけ反射量アップ」といったアレンジがインスペクター操作だけでできるようになります。

【Unity】攻撃を跳ね返せ!「DamageReflection」コンポーネント

まずは、この記事だけで完結するように、シンプルなダメージシステム一式を用意します。

  • ダメージ情報を持つ DamageData
  • ダメージを受ける側の Health(HP管理&ダメージ受付)
  • そして今回の主役 DamageReflection(ダメージ反射)

フルコード


using UnityEngine;

namespace Sample.Combat
{
    /// <summary>
    /// ダメージ情報をまとめるシンプルな構造体。
    /// - amount: ダメージ量
    /// - attacker: 攻撃者(反射先を特定するために必要)
    /// - source: 何由来のダメージか(任意)
    /// </summary>
    [System.Serializable]
    public struct DamageData
    {
        public float amount;
        public GameObject attacker;
        public string source;

        public DamageData(float amount, GameObject attacker, string source = "")
        {
            this.amount = amount;
            this.attacker = attacker;
            this.source = source;
        }
    }

    /// <summary>
    /// シンプルなHP管理コンポーネント。
    /// - TakeDamage を呼ぶことでダメージを受ける
    /// - 0以下になったら OnDie イベントを発火
    /// </summary>
    public class Health : MonoBehaviour
    {
        [Header("HP 設定")]
        [SerializeField] private float maxHealth = 100f;
        [SerializeField] private bool destroyOnDeath = true;

        // 現在HP
        public float CurrentHealth { get; private set; }

        // 死亡通知用の簡易イベント(必要なら UnityEvent に差し替え可)
        public System.Action<Health> OnDie;

        private void Awake()
        {
            CurrentHealth = maxHealth;
        }

        /// <summary>
        /// ダメージを受ける共通入口。
        /// DamageReflection などの別コンポーネントからもここを叩きます。
        /// </summary>
        /// <param name="damage">ダメージ情報</param>
        public void TakeDamage(DamageData damage)
        {
            if (CurrentHealth <= 0f)
            {
                // すでに死亡済みなら無視
                return;
            }

            // ダメージ適用
            CurrentHealth -= damage.amount;

            // 0未満にならないようにクランプ
            if (CurrentHealth < 0f)
            {
                CurrentHealth = 0f;
            }

            // デバッグ用ログ
            Debug.Log(
                $"[{name}] が {damage.attacker?.name ?? "不明な攻撃者"} から " +
                $"{damage.amount} ダメージを受けた。残りHP: {CurrentHealth}",
                this
            );

            // 死亡判定
            if (CurrentHealth <= 0f)
            {
                HandleDeath();
            }
        }

        private void HandleDeath()
        {
            Debug.Log($"[{name}] は倒れた。", this);

            // イベント通知
            OnDie?.Invoke(this);

            // 必要なら GameObject を破壊
            if (destroyOnDeath)
            {
                Destroy(gameObject);
            }
        }

        /// <summary>
        /// HPを全回復するユーティリティ。
        /// デバッグやリトライ用にあると便利。
        /// </summary>
        public void ResetHealth()
        {
            CurrentHealth = maxHealth;
        }
    }

    /// <summary>
    /// ダメージ反射コンポーネント。
    /// Health でダメージを受けたタイミングをフックして、
    /// 受けたダメージの一部を攻撃者にそのまま返します。
    /// 
    /// 責務は「反射すること」だけに限定し、HP管理は Health に任せます。
    /// </summary>
    [RequireComponent(typeof(Health))]
    public class DamageReflection : MonoBehaviour
    {
        [Header("反射設定")]
        [Tooltip("受けたダメージに対して、どれくらいの割合を反射するか(0〜1以上)。例: 0.5 で 50% 反射。")]
        [SerializeField, Range(0f, 2f)]
        private float reflectionRate = 0.5f;

        [Tooltip("反射ダメージの最小値。これ未満にはならない。0で無効。")]
        [SerializeField]
        private float minReflectionDamage = 0f;

        [Tooltip("反射ダメージの最大値。0以下なら無制限。")]
        [SerializeField]
        private float maxReflectionDamage = 0f;

        [Header("オプション")]
        [Tooltip("自分が死亡したときに、最後に攻撃してきた相手へ追加ダメージを与えるか")]
        [SerializeField]
        private bool explodeOnDeath = false;

        [Tooltip("死亡時に与える追加ダメージ量(explodeOnDeath 有効時のみ)")]
        [SerializeField]
        private float deathExplosionDamage = 20f;

        // 内部参照
        private Health _health;

        // 最後に自分へダメージを与えた攻撃者を記録しておく
        private GameObject _lastAttacker;

        private void Awake()
        {
            // Health は RequireComponent で必ず付いている前提
            _health = GetComponent<Health>();

            // Health の死亡イベントを購読
            _health.OnDie += OnOwnerDie;
        }

        private void OnDestroy()
        {
            // イベント購読解除(メモリリークや参照残り防止)
            if (_health != null)
            {
                _health.OnDie -= OnOwnerDie;
            }
        }

        /// <summary>
        /// 外部から「ダメージを受けたこと」を通知してもらうためのメソッド。
        /// 通常は Health.TakeDamage の直前 or 直後に呼び出します。
        /// 
        /// - ここでは「反射する量の計算」と「攻撃者へのダメージ送信」だけを行う
        /// - 実際の HP 減少は Health 側に任せる
        /// </summary>
        /// <param name="incomingDamage">受けたダメージ情報</param>
        public void OnDamageTaken(DamageData incomingDamage)
        {
            // 攻撃者がいない場合は反射できない
            if (incomingDamage.attacker == null)
            {
                return;
            }

            // 直近の攻撃者として記録
            _lastAttacker = incomingDamage.attacker;

            // 反射ダメージを計算
            float reflectAmount = incomingDamage.amount * reflectionRate;

            // 最小・最大値でクランプ
            if (minReflectionDamage > 0f && reflectAmount < minReflectionDamage)
            {
                reflectAmount = minReflectionDamage;
            }

            if (maxReflectionDamage > 0f && reflectAmount > maxReflectionDamage)
            {
                reflectAmount = maxReflectionDamage;
            }

            // 0以下なら何もしない
            if (reflectAmount <= 0f)
            {
                return;
            }

            // 攻撃者側の Health を探す
            var attackerHealth = incomingDamage.attacker.GetComponent<Health>();
            if (attackerHealth == null)
            {
                // 攻撃者がダメージを受けられない存在(壁など)の場合は無視
                return;
            }

            // 反射ダメージ情報を作成
            var reflectedDamage = new DamageData(
                amount: reflectAmount,
                attacker: gameObject, // 反射している自分を攻撃者として扱う
                source: "DamageReflection"
            );

            // 攻撃者にダメージを与える
            attackerHealth.TakeDamage(reflectedDamage);

            Debug.Log(
                $"[{name}] が [{incomingDamage.attacker.name}] に " +
                $"{reflectAmount} ダメージを反射した。",
                this
            );
        }

        /// <summary>
        /// 所有者が死亡したときのコールバック。
        /// Health.OnDie から呼ばれます。
        /// </summary>
        /// <param name="ownerHealth">死亡した Health</param>
        private void OnOwnerDie(Health ownerHealth)
        {
            if (!explodeOnDeath)
            {
                return;
            }

            if (_lastAttacker == null)
            {
                return;
            }

            var attackerHealth = _lastAttacker.GetComponent<Health>();
            if (attackerHealth == null)
            {
                return;
            }

            if (deathExplosionDamage <= 0f)
            {
                return;
            }

            var deathDamage = new DamageData(
                amount: deathExplosionDamage,
                attacker: gameObject,
                source: "DeathExplosion"
            );

            attackerHealth.TakeDamage(deathDamage);

            Debug.Log(
                $"[{name}] の死亡時爆発で [{_lastAttacker.name}] に " +
                $"{deathExplosionDamage} ダメージ!",
                this
            );
        }
    }

    /// <summary>
    /// 簡易的な「攻撃テスター」。
    /// キー入力でターゲットにダメージを与えるだけのサンプルです。
    /// 実際のゲームでは、弾・近接攻撃・スキルなどから Health.TakeDamage を呼ぶ形に置き換えましょう。
    /// </summary>
    public class SimpleAttacker : MonoBehaviour
    {
        [Header("攻撃設定")]
        [SerializeField] private Health targetHealth;
        [SerializeField] private float baseDamage = 10f;
        [SerializeField] private KeyCode attackKey = KeyCode.Space;

        private void Update()
        {
            if (targetHealth == null)
            {
                return;
            }

            if (Input.GetKeyDown(attackKey))
            {
                // ダメージ情報作成
                var damage = new DamageData(
                    amount: baseDamage,
                    attacker: gameObject,
                    source: "SimpleAttack"
                );

                // まずターゲットの DamageReflection に通知(あれば)
                var reflection = targetHealth.GetComponent<DamageReflection>();
                if (reflection != null)
                {
                    reflection.OnDamageTaken(damage);
                }

                // その後、実際にHPを減らす
                targetHealth.TakeDamage(damage);

                Debug.Log(
                    $"[{name}] が [{targetHealth.name}] に {baseDamage} ダメージを与えた。",
                    this
                );
            }
        }
    }
}

使い方の手順

ここでは、プレイヤー vs 敵 のシンプルな例で使い方を説明します。

  1. ① プレイヤーと敵の GameObject を用意する
    • Hierarchy に PlayerEnemy を作成
    • どちらにも Health コンポーネントを追加
    • HP の最大値(maxHealth)をお好みで設定(例: Player=100, Enemy=50)
  2. ② 敵にダメージ反射を付与する
    • Enemy オブジェクトに DamageReflection コンポーネントを追加
    • reflectionRate を 0.5 にすると「受けたダメージの 50% を反射」
    • minReflectionDamage を 1 にすると「どんな小さい攻撃でも最低 1 ダメージは返す」
    • maxReflectionDamage を 20 にすると「一度に返すダメージは最大 20 まで」
    • explodeOnDeath を ON にして deathExplosionDamage を 30 にすると、
      敵が倒れた瞬間に最後の攻撃者へ 30 ダメージを与える「道連れ」仕様になります。
  3. ③ 攻撃側に SimpleAttacker を付ける
    • PlayerSimpleAttacker コンポーネントを追加
    • インスペクターで targetHealthEnemyHealth をドラッグ&ドロップ
    • baseDamage を 10、attackKey を Space のままにしておきます
    • 再生して Space キーを押すと、Player → Enemy に 10 ダメージが入り、
      同時に Enemy 側から Player に反射ダメージが返ります
  4. ④ 他の用途への応用例
    • トゲ床(ダメージ床)HealthDamageReflection を付けておくと、
      プレイヤーが攻撃しても逆にダメージを受ける「攻撃してはいけないトラップ」になります。
    • ボス戦で「第2フェーズからだけダメージ反射を有効化」したい場合は、
      DamageReflection.enabled = true/false をフェーズ管理スクリプトから切り替えるだけで済みます。
    • PvP キャラにだけ反射コンポーネントを付けておけば、
      「反射ビルド」「カウンター型キャラ」といった個性を、プレハブの組み合わせだけで表現できます。

メリットと応用

DamageReflection を独立したコンポーネントに分離しておくと、次のようなメリットがあります。

  • 責務が小さいのでコードが読みやすく、バグ調査もしやすい
  • プレハブの組み合わせで挙動を変えられる(「この敵は反射」「この敵はしない」など)
  • HP管理 (Health) と反射ロジックが分かれているので、他のゲームでもコピペ再利用しやすい
  • インスペクター上の 数値調整だけでゲームバランスを変えられる(反射率・上限・爆発ダメージなど)

レベルデザイン的にも、「この部屋の敵は全員ダメージ反射持ち」「このトラップだけ反射倍率2倍」といったアレンジが、スクリプトを一行も書き換えずにプレハブ設定だけで完結するようになります。

さらに発展させたい場合、例えば「一定時間だけ反射率を上げるバフ」のような要素も簡単に追加できます。下記は DamageReflection に追加できる、一時的に反射率を変更するメソッドの例です。


        /// <summary>
        /// 一定時間だけ反射率を変更する簡易バフ。
        /// 例: StartCoroutine(TemporaryBoost(1.5f, 5f));
        /// </summary>
        /// <param name="multiplier">現在の反射率に掛ける倍率</param>
        /// <param name="duration">持続時間(秒)</param>
        public System.Collections.IEnumerator TemporaryBoost(float multiplier, float duration)
        {
            float originalRate = reflectionRate;
            reflectionRate *= multiplier;

            yield return new WaitForSeconds(duration);

            reflectionRate = originalRate;
        }

このように、「ダメージ反射」という単一の責務に絞ったコンポーネントとして設計しておくと、あとから仕様を足したり他のシステムと組み合わせたりするのがとても楽になります。巨大な God クラスにロジックを詰め込むのではなく、小さなコンポーネントを組み合わせてゲームを作っていきましょう。