Unityを触り始めると、つい「とりあえず全部Updateに書く」実装になりがちですよね。
プレイヤー操作、ダメージ計算、UI更新、エフェクト制御…全部ひとつのスクリプトに押し込んでしまうと、次のような問題が出てきます。

  • 処理の見通しが悪くなり、バグ修正が怖くなる
  • 敵やボスにも同じロジックを使いたいのに、コピペ地獄になる
  • 「ちょっと吸血効果を入れたい」だけなのに、巨大クラスを編集する必要がある

そこでおすすめなのが、「1つのスクリプト = 1つの責務」を意識したコンポーネント指向の設計です。
今回は「攻撃ヒット時に与えたダメージの一部を回復する」吸血攻撃を、単独で付け外しできるコンポーネントとして実装してみましょう。

【Unity】殴るほど回復する吸血ビルド!「Lifesteal」コンポーネント

この記事では、攻撃ヒット時に与えたダメージの一定割合を自分のHPとして回復する「Lifesteal」コンポーネントを実装します。
ポイントは以下です。

  • HP管理は専用コンポーネント Health に分離
  • ダメージ通知は Damageable コンポーネント経由のイベントでやり取り
  • Lifesteal は「ダメージを与えたときのフック」だけに責務を絞る

これにより、

  • プレイヤーだけでなく、敵やボスにも簡単に吸血効果を付与できる
  • 吸血の有無を「コンポーネントを付けるかどうか」で切り替えられる
  • ダメージ処理の中心ロジックを汚さずに済む

それでは、必要なコンポーネント一式のコードから見ていきましょう。


フルコード一式

以下の3コンポーネントを用意します。

  1. Health … HPの管理(ダメージ・回復・死亡イベント)
  2. Damageable … ダメージを受ける対象に付ける「被ダメージ窓口」
  3. Lifesteal … 与えたダメージの一部を回復する吸血コンポーネント

1. Health.cs(HP管理)


using UnityEngine;
using UnityEngine.Events;

/// <summary>シンプルなHP管理コンポーネント</summary>
public class Health : MonoBehaviour
{
    [Header("HP 設定")]
    [SerializeField] private float maxHealth = 100f;
    [SerializeField] private float currentHealth = 100f;

    [Header("イベント")]
    [SerializeField] private UnityEvent onDeath;
    [SerializeField] private UnityEvent onDamaged;   // 受けたダメージ量
    [SerializeField] private UnityEvent onHealed;    // 回復量

    public float MaxHealth => maxHealth;
    public float CurrentHealth => currentHealth;
    public bool IsDead => currentHealth <= 0f;

    private void Awake()
    {
        // エディタ上で currentHealth を変更していない場合は maxHealth で初期化
        if (Mathf.Approximately(currentHealth, 0f))
        {
            currentHealth = maxHealth;
        }
    }

    /// <summary>ダメージを受ける(マイナス値を渡さないこと)</summary>
    public void TakeDamage(float amount)
    {
        if (IsDead) return;
        if (amount <= 0f) return;

        currentHealth = Mathf.Max(0f, currentHealth - amount);
        onDamaged?.Invoke(amount);

        if (IsDead)
        {
            onDeath?.Invoke();
        }
    }

    /// <summary>回復する(マイナス値を渡さないこと)</summary>
    public void Heal(float amount)
    {
        if (IsDead) return;
        if (amount <= 0f) return;

        float oldHealth = currentHealth;
        currentHealth = Mathf.Min(maxHealth, currentHealth + amount);
        float healedAmount = currentHealth - oldHealth;

        if (healedAmount > 0f)
        {
            onHealed?.Invoke(healedAmount);
        }
    }

    /// <summary>HPを最大値まで全回復</summary>
    public void RestoreFull()
    {
        float amount = maxHealth - currentHealth;
        Heal(amount);
    }
}

2. Damageable.cs(ダメージ窓口)


using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 「このオブジェクトはダメージを受けられる」ことを表すコンポーネント。
/// 攻撃側はこのコンポーネントに対して Damage() を呼ぶだけにすると、
/// ダメージ処理の責務を分離しやすくなります。
/// </summary>
[RequireComponent(typeof(Health))]
public class Damageable : MonoBehaviour
{
    [SerializeField] private Health health;

    // 「誰から」「どれだけ」ダメージを受けたかのイベント
    [System.Serializable]
    public class DamageEvent : UnityEvent<GameObject, float> { }

    [Header("ダメージイベント")]
    [SerializeField] private DamageEvent onDamagedBy;

    public DamageEvent OnDamagedBy => onDamagedBy;

    private void Reset()
    {
        health = GetComponent<Health>();
    }

    private void Awake()
    {
        if (health == null)
        {
            health = GetComponent<Health>();
        }
    }

    /// <summary>
    /// ダメージを適用するメイン入口。
    /// attacker: 攻撃者(null でも可)
    /// amount  : ダメージ量(0以上)
    /// </summary>
    public void Damage(GameObject attacker, float amount)
    {
        if (amount <= 0f) return;

        // まずHPを減らす
        health.TakeDamage(amount);

        // その後、誰からどれだけダメージを受けたかを通知
        onDamagedBy?.Invoke(attacker, amount);
    }
}

3. Lifesteal.cs(吸血攻撃)


using UnityEngine;

/// <summary>
/// 攻撃ヒット時に与えたダメージの一定割合を自分のHPとして回復するコンポーネント。
/// 
/// 使い方の想定:
/// - 攻撃を行うオブジェクト(プレイヤー、敵など)にアタッチ
/// - 攻撃がヒットしたタイミングで OnAttackHit() を呼ぶ
///   (例:近接武器のトリガー、弾の衝突判定など)
/// 
/// SRP的には:
/// - 「いつダメージを与えるか」は別コンポーネント
/// - 「与えたダメージから何%吸血するか」だけをこのコンポーネントが担当
/// </summary>
[RequireComponent(typeof(Health))]
public class Lifesteal : MonoBehaviour
{
    [Header("吸血設定")]
    [Tooltip("与えたダメージの何%を回復するか(0〜1)。例: 0.2 で 20%")]
    [SerializeField, Range(0f, 1f)]
    private float lifestealRate = 0.2f;

    [Tooltip("1ヒットで回復できる最大値。0以下なら制限なし")]
    [SerializeField]
    private float maxHealPerHit = 0f;

    [Header("参照")]
    [SerializeField] private Health selfHealth;

    private void Reset()
    {
        selfHealth = GetComponent<Health>();
    }

    private void Awake()
    {
        if (selfHealth == null)
        {
            selfHealth = GetComponent<Health>();
        }
    }

    /// <summary>
    /// 攻撃がヒットしてダメージを与えたときに呼ぶ関数。
    /// 
    /// damageDealt: 実際に相手に与えたダメージ量
    /// target     : 攻撃対象(使わない場合は null でOK。将来の拡張用)
    /// </summary>
    public void OnAttackHit(float damageDealt, GameObject target)
    {
        if (damageDealt <= 0f) return;
        if (selfHealth == null) return;
        if (selfHealth.IsDead) return;

        // 吸血量を計算
        float healAmount = damageDealt * lifestealRate;

        // 1ヒットあたりの最大回復量が設定されていれば制限
        if (maxHealPerHit > 0f)
        {
            healAmount = Mathf.Min(healAmount, maxHealPerHit);
        }

        if (healAmount <= 0f) return;

        // 自分自身を回復
        selfHealth.Heal(healAmount);

        // デバッグ用ログ(必要なければ削除)
        // Debug.Log($"{gameObject.name} は吸血で {healAmount} 回復しました。");
    }
}

4. おまけ:シンプルな近接攻撃例(任意)

「いつ OnAttackHit を呼ぶか」の一例として、
レイキャストで前方の敵にダメージを与える簡易近接攻撃コンポーネントを載せておきます。


using UnityEngine;

/// <summary>
/// 非常にシンプルな近接攻撃例。
/// - 左クリックで前方にレイを飛ばし、
///   Damageable を持つ対象にダメージを与える。
/// - Lifesteal を持っていれば、与えたダメージに応じて回復する。
/// 
/// 実運用では InputSystem やアニメーションイベントと連携させると良いです。
/// </summary>
[RequireComponent(typeof(Lifesteal))]
public class SimpleMeleeAttack : MonoBehaviour
{
    [Header("攻撃設定")]
    [SerializeField] private float damage = 20f;
    [SerializeField] private float range = 2f;
    [SerializeField] private LayerMask hitLayers = ~0; // 全レイヤー対象

    [Header("参照")]
    [SerializeField] private Lifesteal lifesteal;

    private void Reset()
    {
        lifesteal = GetComponent<Lifesteal>();
    }

    private void Awake()
    {
        if (lifesteal == null)
        {
            lifesteal = GetComponent<Lifesteal>();
        }
    }

    private void Update()
    {
        // デモ用にマウス左クリックで攻撃
        if (Input.GetMouseButtonDown(0))
        {
            PerformAttack();
        }
    }

    private void PerformAttack()
    {
        Ray ray = new Ray(transform.position, transform.forward);
        if (Physics.Raycast(ray, out RaycastHit hit, range, hitLayers, QueryTriggerInteraction.Ignore))
        {
            Damageable damageable = hit.collider.GetComponentInParent<Damageable>();
            if (damageable != null)
            {
                // 実際にダメージを与える
                damageable.Damage(gameObject, damage);

                // 吸血コンポーネントに「このダメージ量でヒットしたよ」と通知
                lifesteal.OnAttackHit(damage, damageable.gameObject);
            }
        }
    }
}

使い方の手順

手順①:スクリプトをプロジェクトに追加

  1. Unity の Project ウィンドウで、Scripts フォルダなどを作成。
  2. そこに以下の4つのC#ファイルを作成して、上記コードをそれぞれ貼り付けます。
    • Health.cs
    • Damageable.cs
    • Lifesteal.cs
    • SimpleMeleeAttack.cs(デモ用・任意)

手順②:ダメージを受ける側(敵など)にコンポーネントを付ける

例として「Enemy」プレハブを作る場合:

  1. Hierarchy で Enemy 用の GameObject を作成(3Dなら Capsule など)。
  2. Enemy に以下のコンポーネントを追加。
    • Health … HP上限や初期値を設定(例:Max 100)
    • Damageable … 自動で Health が参照されます
  3. 必要に応じて HealthonDeath イベントに「Destroy(this.gameObject)」などを登録すると、HP0で消える敵が作れます。

手順③:攻撃する側(プレイヤーなど)に Lifesteal を付ける

例として「Player」オブジェクトに吸血近接攻撃を持たせる場合:

  1. Hierarchy で Player オブジェクトを用意。
  2. Player に以下のコンポーネントを追加。
    • Health … プレイヤーのHP管理
    • Lifesteal … 吸血設定
      • Lifesteal Rate:0.2(= 20%)など
      • Max Heal Per Hit:0(制限なし)か、1ヒットでの最大回復量を任意で設定
    • SimpleMeleeAttack … デモ用近接攻撃(攻撃ロジックを自作している場合は不要)
  3. SimpleMeleeAttackDamageRange をお好みで調整。

これで「Player が左クリックで近接攻撃 → Enemy にダメージ → 与えたダメージの一部を Player が回復」という流れが動くはずです。

手順④:別の例(動く床やボスにも使う)

このコンポーネント設計の良いところは、攻撃する側に Lifesteal を付けるだけで吸血ビルドにできる点です。

  • 敵のボスに Lifesteal を付ける
    • ボスの GameObject に HealthLifesteal を追加。
    • ボスの攻撃ロジック(近接・遠距離問わず)で、ヒットしたときに Lifesteal.OnAttackHit(実ダメージ, 対象) を呼ぶ。
    • これだけで「殴ると回復するいやらしいボス」が作れます。
  • トゲ付き動く床
    • 床オブジェクトに「プレイヤーにダメージを与える」スクリプトを付ける。
    • その床に HealthLifesteal を付ければ、「踏むとダメージ+床が回復する」ギミックにもできます(ギミックHPが減るゲーム設計の場合)。

メリットと応用

このように Lifesteal を「与えたダメージに応じて回復するだけの小さなコンポーネント」として切り出しておくと、プレハブ管理やレベルデザインがかなり楽になります。

メリット

  • プレハブの組み合わせでビルドを表現できる
    プレイヤーの「ビルド変更」を、巨大な Player スクリプトの if 文ではなく、
    Lifesteal を付けたり外したりするだけで表現できます。
    吸血ビルド・クリティカルビルド・毒ビルドなどを、コンポーネント単位で差し替えられます。
  • 敵AIとダメージ処理を分離できる
    敵AIスクリプトは「いつ攻撃するか」だけに集中させ、
    ダメージと吸血は Damageable / Lifesteal に任せられます。
    これにより、AIロジックの見通しが良くなります。
  • テストしやすい
    Lifesteal 単体をテストしたい場合、
    シーンにダミーの Health を持つオブジェクトを置いて、
    インスペクターから OnAttackHit(50, null) を呼ぶだけで挙動確認ができます。

応用アイデア

  • 「HPが一定以下のときだけ吸血量を増やす」狂戦士ビルド
  • 「特定の属性の敵にだけ吸血が有効」な属性システムとの連携
  • 「最後に与えたダメージ量をUIに表示」して、吸血感を演出

例えば、「HPが50%未満のときは吸血量を2倍にする」改造案はこんな感じです。


private void ApplyBerserkBonus(ref float healAmount)
{
    // HPが50%未満なら吸血量2倍のバーサーカーモード
    if (selfHealth != null && selfHealth.CurrentHealth < selfHealth.MaxHealth * 0.5f)
    {
        healAmount *= 2f;
    }
}

この関数を Lifesteal.OnAttackHit 内で、healAmount を確定する前に呼び出すだけで、
「瀕死になるほど吸血力が上がる」ビルドに簡単に拡張できます。

こういった小さなコンポーネントを積み重ねていくと、
巨大な God クラスに頼らない、保守しやすい Unity プロジェクトになっていきます。
ぜひ自分のプロジェクトでも、「攻撃ロジック」と「吸血ロジック」を分ける設計を試してみてください。