Unityを触り始めると、つい「とりあえず全部Updateに書く」実装をしがちですよね。
プレイヤーの入力処理、移動、アニメーション、HP管理、ダメージ処理…すべてを1つのスクリプトに押し込んでしまうと、次のような問題が出てきます。

  • ちょっと仕様を変えたいだけなのに、巨大なスクリプト全体を読まないといけない
  • 敵キャラや味方キャラにも同じ処理を使い回したいのに、コピペが増えてバグの温床になる
  • HPやシールドなどのパラメータ管理がごちゃごちゃして、テストもしづらい

そこで今回は、「ダメージを受けたとき、HPより先に消費されるシールド値」だけをきれいに切り出した、
コンポーネント指向なシールド管理クラスを作ってみましょう。

この記事で紹介する 「ShieldGenerator」コンポーネント を使えば、

  • シールドの現在値・最大値の管理
  • シールドでダメージを肩代わりしてからHPに通す処理
  • シールドの自動回復(クールダウン付き)

といった処理を1つの小さな責務にまとめることができます。
プレイヤーにも敵にも、さらには動く防御壁などにも、同じコンポーネントをポン付けして再利用できるようになります。


【Unity】HPの前にシールドで守る!「ShieldGenerator」コンポーネント

まずは、コピペしてそのまま使えるフルコードからどうぞ。


using UnityEngine;

/// <summary>
/// シールド値を管理し、ダメージをHPより先に肩代わりするコンポーネント。
/// - シールドの現在値 / 最大値の管理
/// - シールドによるダメージ軽減(肩代わり)
/// - 自動回復(クールダウン付き)
///
/// 「HPそのもの」は別コンポーネント(例: Health)で管理し、
/// このクラスは「シールドの責務」にだけ集中させる設計を想定しています。
/// </summary>
public class ShieldGenerator : MonoBehaviour
{
    // --- シールドの基本パラメータ ---

    [Header("シールド基本設定")]
    [Tooltip("シールドの最大値")]
    [SerializeField] private float maxShield = 100f;

    [Tooltip("ゲーム開始時の初期シールド値(通常は最大値と同じにする)")]
    [SerializeField] private float initialShield = 100f;

    // --- 自動回復設定 ---

    [Header("自動回復設定")]
    [Tooltip("シールドが 0 でなくても自動回復するかどうか")]
    [SerializeField] private bool canRegenerate = true;

    [Tooltip("1秒あたりのシールド回復量")]
    [SerializeField] private float regenRatePerSecond = 10f;

    [Tooltip("ダメージを受けてから自動回復が始まるまでの待ち時間(秒)")]
    [SerializeField] private float regenDelay = 3f;

    // --- デバッグ・イベント用 ---

    [Header("デバッグ用")]
    [Tooltip("現在のシールド値(インスペクタで確認用)")]
    [SerializeField] private float currentShield;

    [Tooltip("最後にダメージを受けた時間(インスペクタで確認用)")]
    [SerializeField] private float lastDamageTime = Mathf.NegativeInfinity;

    /// <summary>現在のシールド値(読み取り専用)</summary>
    public float CurrentShield => currentShield;

    /// <summary>シールドの最大値(読み取り専用)</summary>
    public float MaxShield => maxShield;

    /// <summary>シールドが最大値かどうか</summary>
    public bool IsShieldFull => Mathf.Approximately(currentShield, maxShield);

    /// <summary>シールドが 0 かどうか</summary>
    public bool IsShieldEmpty => currentShield <= 0f;

    private void Awake()
    {
        // 初期シールド値をクランプして設定
        currentShield = Mathf.Clamp(initialShield, 0f, maxShield);
    }

    private void Update()
    {
        HandleRegeneration();
    }

    /// <summary>
    /// 外部からダメージを適用するためのメイン入口。
    /// 返り値として「HPに通すべき残りダメージ量」を返します。
    ///
    /// 例:
    ///   float remaining = shield.ApplyDamage(incomingDamage);
    ///   health.ApplyDamage(remaining);
    /// </summary>
    /// <param name="damageAmount">受けたダメージ量(0以上を想定)</param>
    /// <returns>シールドで吸収しきれなかった残りダメージ量</returns>
    public float ApplyDamage(float damageAmount)
    {
        if (damageAmount <= 0f)
        {
            // 負数や0はそのまま返す(ノーダメージ)
            return 0f;
        }

        // 最後にダメージを受けた時間を記録(自動回復のクールダウン用)
        lastDamageTime = Time.time;

        if (IsShieldEmpty)
        {
            // シールドが空なら、すべてHPに通す
            return damageAmount;
        }

        // 実際にシールドで吸収できるダメージ量
        float absorbed = Mathf.Min(currentShield, damageAmount);

        // シールドを減らす
        currentShield -= absorbed;
        currentShield = Mathf.Max(currentShield, 0f);

        // 吸収しきれなかった残りダメージ量
        float remaining = damageAmount - absorbed;
        return remaining;
    }

    /// <summary>
    /// シールドを即時に回復させるメソッド。
    /// マイナス値を渡すと「減らす」こともできます(例: デバフやシールド破壊攻撃)。
    /// </summary>
    /// <param name="amount">増減させるシールド量(正で回復、負で減少)</param>
    public void ModifyShield(float amount)
    {
        currentShield = Mathf.Clamp(currentShield + amount, 0f, maxShield);
    }

    /// <summary>
    /// シールドを最大値まで全回復させる。
    /// 例: 回復アイテムやチェックポイント到達時など。
    /// </summary>
    public void RestoreToFull()
    {
        currentShield = maxShield;
    }

    /// <summary>
    /// シールドの自動回復処理。
    /// Update から呼び出されます。
    /// </summary>
    private void HandleRegeneration()
    {
        if (!canRegenerate)
        {
            return;
        }

        // シールドが既に最大なら何もしない
        if (IsShieldFull)
        {
            return;
        }

        // ダメージを受けてから一定時間経過していなければ回復しない
        if (Time.time - lastDamageTime < regenDelay)
        {
            return;
        }

        // 経過時間に応じて徐々に回復
        float regenAmount = regenRatePerSecond * Time.deltaTime;
        currentShield = Mathf.Clamp(currentShield + regenAmount, 0f, maxShield);
    }

    /// <summary>
    /// シールド値を 0〜1 の割合で取得する。
    /// UI スライダーなどにそのまま渡せる便利メソッド。
    /// </summary>
    /// <returns>0〜1 のシールド割合</returns>
    public float GetShieldRatio()
    {
        if (maxShield <= 0f) return 0f;
        return currentShield / maxShield;
    }

    /// <summary>
    /// シールド設定を動的に変更したいとき用。
    /// 例: アイテムで最大シールドを増やす。
    /// </summary>
    /// <param name="newMaxShield">新しい最大シールド値</param>
    /// <param name="keepCurrentRatio">
    /// true: 現在の割合を維持して再計算
    /// false: 現在値はクランプのみ
    /// </param>
    public void SetMaxShield(float newMaxShield, bool keepCurrentRatio = true)
    {
        newMaxShield = Mathf.Max(0f, newMaxShield);

        if (keepCurrentRatio && maxShield > 0f)
        {
            float ratio = currentShield / maxShield;
            maxShield = newMaxShield;
            currentShield = Mathf.Clamp(maxShield * ratio, 0f, maxShield);
        }
        else
        {
            maxShield = newMaxShield;
            currentShield = Mathf.Clamp(currentShield, 0f, maxShield);
        }
    }
}

使い方の手順

ここでは、典型的な「プレイヤー+HP+シールド」の例をベースにしつつ、
敵キャラや動く防御壁にも応用できる形で解説します。

手順①:HP管理コンポーネントを用意する

ShieldGenerator は「HPそのもの」は持ちません。
責務を分けるために、HPは別コンポーネントに任せましょう。簡単な例を示します。


using UnityEngine;

/// <summary>
/// シンプルなHP管理コンポーネント。
/// ShieldGenerator と組み合わせて使う前提の最小実装です。
/// </summary>
public class Health : MonoBehaviour
{
    [SerializeField] private float maxHp = 100f;
    [SerializeField] private float currentHp;

    public float CurrentHp => currentHp;
    public float MaxHp => maxHp;
    public bool IsDead => currentHp <= 0f;

    private void Awake()
    {
        currentHp = maxHp;
    }

    public void ApplyDamage(float damage)
    {
        if (damage <= 0f || IsDead) return;

        currentHp -= damage;
        currentHp = Mathf.Max(0f, currentHp);

        if (IsDead)
        {
            Debug.Log($"{name} は倒れました");
            // TODO: 死亡アニメーションやリスポーン処理など
        }
    }

    public void Heal(float amount)
    {
        if (amount <= 0f || IsDead) return;

        currentHp = Mathf.Clamp(currentHp + amount, 0f, maxHp);
    }
}

手順②:プレイヤーにコンポーネントをアタッチする

  1. Unity エディタでプレイヤーの GameObject を選択
  2. Add Component から
    • Health
    • ShieldGenerator

    を追加

  3. ShieldGenerator のインスペクタで
    • Max Shield(最大シールド)
    • Initial Shield(初期シールド)
    • Regen Rate Per Second(1秒あたりの回復量)
    • Regen Delay(ダメージ後、回復開始までの秒数)

    をお好みの値に設定

これで、プレイヤーに「HP+シールド」の器が用意できました。

手順③:ダメージを与える側から呼び出す

次に、敵の攻撃やトラップからダメージを与える処理を書きます。
ポイントは、必ず ShieldGenerator → Health の順にダメージを通すことです。


using UnityEngine;

/// <summary>
/// 何かに触れたらダメージを与える単純なトラップの例。
/// プレイヤーでも敵でも、Health と ShieldGenerator が付いていれば動きます。
/// </summary>
public class DamageOnTrigger : MonoBehaviour
{
    [SerializeField] private float damageAmount = 30f;

    private void OnTriggerEnter(Collider other)
    {
        // 相手が Health を持っているか確認
        Health health = other.GetComponent<Health>();
        if (health == null)
        {
            return;
        }

        // シールドを持っているか確認(なくてもOK)
        ShieldGenerator shield = other.GetComponent<ShieldGenerator>();

        float remainingDamage = damageAmount;

        // シールドがあれば、まずシールドでダメージを肩代わり
        if (shield != null)
        {
            remainingDamage = shield.ApplyDamage(damageAmount);
        }

        // 残りダメージをHPに適用
        if (remainingDamage > 0f)
        {
            health.ApplyDamage(remainingDamage);
        }

        Debug.Log($"{other.name} に {damageAmount} ダメージ(シールド吸収後の残り: {remainingDamage})");
    }
}

このように、ダメージ処理の入口は常に「ShieldGenerator.ApplyDamage」にしておくと、
シールドの仕様変更(回復速度、最大値、ON/OFF など)をコンポーネント単体で調整できるようになります。

手順④:敵や動く防御壁にも再利用する

  • 敵キャラ:ボスだけシールドを持たせたい場合は、ボスのプレハブに
    • Health
    • ShieldGenerator

    を追加して、雑魚敵には ShieldGenerator を付けないだけで差別化できます。

  • 動く防御壁:プレイヤーを守るシールド壁オブジェクトに ShieldGenerator を付け、
    DamageOnTrigger のような攻撃を受けるたびにシールドが削れていく演出が可能です。

どのケースでも、「ダメージを受ける側」は Health / ShieldGenerator を持つだけ
「ダメージを与える側」は DamageOnTrigger などの攻撃コンポーネントを持つだけ、という分離ができているのがポイントです。


メリットと応用

メリット①:プレハブ設計がシンプルになる

ShieldGenerator を使うと、プレハブの設計が次のように整理されます。

  • 共通:Health(すべてのダメージを受けるオブジェクトが持つ)
  • シールド持ちだけ:ShieldGenerator(プレイヤー、ボス、特殊な防御壁など)

この構成にしておけば、

  • 「この敵はシールドなしでHPだけ」→ ShieldGenerator を外すだけ
  • 「このオブジェクトはシールドだけで、HPは存在しない」→ Health を外し、シールドが 0 になったら破壊する処理を追加

といったバリエーションを、プレハブの構成レベルで簡単に作り分けられます。

メリット②:レベルデザイン時のパラメータ調整がしやすい

シールドの最大値、回復速度、回復開始までのクールダウンなどを、
敵ごと・ステージギミックごとにインスペクタで調整できるのが大きな利点です。

  • 序盤のボス:最大シールド少なめ、回復遅め
  • 終盤のボス:最大シールド多め、回復速い、クールダウン短い
  • 特殊ギミック:シールドは自動回復しない(canRegenerate を OFF)

といった調整が、コードを書き換えずにプレハブのパラメータだけで完結します。

メリット③:UIとの連携が分かりやすい

ShieldGenerator には GetShieldRatio() が用意されているので、
UI スライダーやゲージにそのままバインドできます。

たとえば、プレイヤーのシールドゲージを更新するコンポーネントはこんな感じで書けます。


using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// シールド割合を UI スライダーに反映させる簡単な例。
/// </summary>
public class ShieldBar : MonoBehaviour
{
    [SerializeField] private ShieldGenerator shield;
    [SerializeField] private Slider slider;

    private void Reset()
    {
        // 同じ GameObject 上にあるコンポーネントを自動取得(エディタでの補助用)
        if (shield == null)
        {
            shield = GetComponentInParent<ShieldGenerator>();
        }
        if (slider == null)
        {
            slider = GetComponent<Slider>();
        }
    }

    private void Update()
    {
        if (shield == null || slider == null) return;

        slider.value = shield.GetShieldRatio();
    }
}

UI 専用の責務を別コンポーネントに分けているので、
シールドのロジックと見た目の更新がきれいに分離されています。

改造案:シールドブレイク演出を追加する

もう一歩踏み込んで、シールドが 0 になった瞬間にだけエフェクトを出すような改造も簡単です。
以下のようなメソッドを ShieldGenerator に追加して、ApplyDamage 内から呼び出すのも良いですね。


    /// <summary>
    /// シールドが 0 になった瞬間に呼び出す処理の例。
    /// パーティクル再生やサウンド再生などをここにまとめる。
    /// </summary>
    private void OnShieldBroken()
    {
        Debug.Log($"{name} のシールドが破壊されました");

        // ここにパーティクルやSEを仕込む:
        // if (breakEffect != null) Instantiate(breakEffect, transform.position, Quaternion.identity);
        // if (audioSource != null && breakClip != null) audioSource.PlayOneShot(breakClip);
    }

そして ApplyDamage の中で、
「シールドが 0 になったタイミングを検出して OnShieldBroken を呼ぶ」ようにすれば、
ロジックと演出をきれいにまとめたシールドシステムが完成します。

このように、小さな責務ごとにコンポーネントを分けていくと、
あとからの改造や差し替えがとても楽になります。
ぜひ、自分のプロジェクトでも「HP」「シールド」「UI」「エフェクト」など、
役割ごとにスクリプトを分割していきましょう。