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);
}
}
手順②:プレイヤーにコンポーネントをアタッチする
- Unity エディタでプレイヤーの GameObject を選択
- Add Component から
HealthShieldGenerator
を追加
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 など)をコンポーネント単体で調整できるようになります。
手順④:敵や動く防御壁にも再利用する
- 敵キャラ:ボスだけシールドを持たせたい場合は、ボスのプレハブに
HealthShieldGenerator
を追加して、雑魚敵には 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」「エフェクト」など、
役割ごとにスクリプトを分割していきましょう。
