Unityを触り始めた頃って、つい1つのスクリプトのUpdateに全部書いてしまうことが多いですよね。
「ダメージ処理」「死亡処理」「エフェクト再生」「スコア加算」などを1つのクラスに押し込むと、すぐにGodクラス化してしまいます。
- どこでHPが減っているのか分からない
- 敵とプレイヤーで似たようなコードをコピペしてバグの温床になる
- 死亡演出だけ変えたいのに巨大クラスを編集しないといけない
こういう問題は、「HPの管理」だけを担当する小さなコンポーネントを用意しておくと一気に解決できます。
この記事では、HPを持ち、0になったらdiedシグナル(イベント)を発行し、親を消す or アニメーションを呼ぶだけに責務を絞ったコンポーネント
「HealthManager」
をUnity6 / C#で実装していきます。
【Unity】死亡処理をイベントでスッキリ分離!「HealthManager」コンポーネント
以下に、コピペでそのまま使えるフルコードを載せます。
HP管理・ダメージ処理・死亡イベント通知・自動削除/アニメーション呼び出しまでを、1つのコンポーネントにまとめています。
using System;
using UnityEngine;
namespace Sample
{
/// <summary>
/// シンプルなHP管理コンポーネント。
/// - currentHealth が 0 以下になったら died イベントを発行
/// - 死亡時にオブジェクトを破棄する or アニメーションを再生する
///
/// 「HPを持つ存在」が共通で使えるように、処理を1か所に集約します。
/// </summary>
public class HealthManager : MonoBehaviour
{
// ====== 設定項目(インスペクターから編集)======
[Header("基本設定")]
[Tooltip("最大HP。ゲーム開始時はこの値で初期化されます。")]
[SerializeField] private int maxHealth = 100;
[Tooltip("現在HP。Startで maxHealth にリセットされます。")]
[SerializeField] private int currentHealth = 100;
[Header("死亡時の挙動")]
[Tooltip("死亡時に自動的に GameObject を Destroy するかどうか。")]
[SerializeField] private bool destroyOnDeath = true;
[Tooltip("destroyOnDeath が false の場合、ここで指定した Animator に死亡アニメーションを再生させます。")]
[SerializeField] private Animator targetAnimator;
[Tooltip("死亡アニメーションのトリガー名。Animator に同名のTriggerパラメータを用意してください。")]
[SerializeField] private string deathTriggerName = "Die";
[Header("デバッグ用")]
[Tooltip("ダメージテスト用。チェックを入れている間、Dキーでダメージを与えます。")]
[SerializeField] private bool enableDebugDamage = false;
[Tooltip("デバッグ用のダメージ量。")]
[SerializeField] private int debugDamageAmount = 10;
// ====== 公開イベント ======
/// <summary>
/// HPが0になったときに発火するイベント。
/// 他のコンポーネントはこれを購読して、スコア加算やSE再生などを行えます。
/// </summary>
public event Action Died;
/// <summary>
/// HPが変化したときに発火するイベント。
/// UIへの反映などに使えます。
/// (current, max) を引数として渡します。
/// </summary>
public event Action<int, int> HealthChanged;
// ====== プロパティ(読み取り専用)======
/// <summary>
/// 最大HP(読み取り専用)
/// </summary>
public int MaxHealth => maxHealth;
/// <summary>
/// 現在HP(読み取り専用)
/// </summary>
public int CurrentHealth => currentHealth;
/// <summary>
/// すでに死亡処理を実行済みかどうか。
/// 二重に死亡処理が走るのを防ぎます。
/// </summary>
public bool IsDead { get; private set; }
// ====== Unity ライフサイクル ======
private void Start()
{
// ゲーム開始時にHPを最大値で初期化
currentHealth = Mathf.Clamp(currentHealth, 0, maxHealth);
NotifyHealthChanged();
}
private void Update()
{
// デバッグ用:Dキーでダメージを与えて挙動を確認
if (!enableDebugDamage) return;
if (Input.GetKeyDown(KeyCode.D))
{
ApplyDamage(debugDamageAmount);
}
}
// ====== 公開メソッド ======
/// <summary>
/// ダメージを与えるメソッド。
/// 正の値を渡してください(負値は無視されます)。
/// </summary>
/// <param name="amount">与えるダメージ量(正の整数)</param>
public void ApplyDamage(int amount)
{
if (IsDead) return; // すでに死亡済みなら何もしない
if (amount <= 0) return; // 不正な値は無視
currentHealth -= amount;
currentHealth = Mathf.Clamp(currentHealth, 0, maxHealth);
NotifyHealthChanged();
if (currentHealth <= 0)
{
HandleDeath();
}
}
/// <summary>
/// HPを回復するメソッド。
/// 正の値を渡してください(負値は無視されます)。
/// </summary>
/// <param name="amount">回復量(正の整数)</param>
public void Heal(int amount)
{
if (IsDead) return; // 死亡後は回復しない
if (amount <= 0) return; // 不正な値は無視
currentHealth += amount;
currentHealth = Mathf.Clamp(currentHealth, 0, maxHealth);
NotifyHealthChanged();
}
/// <summary>
/// HPを最大値にリセットします。
/// リスポーン時などに使えます。
/// </summary>
public void ResetHealth()
{
IsDead = false;
currentHealth = maxHealth;
NotifyHealthChanged();
}
// ====== 内部処理 ======
/// <summary>
/// HPが変化したことをイベントで通知する。
/// </summary>
private void NotifyHealthChanged()
{
HealthChanged?.Invoke(currentHealth, maxHealth);
}
/// <summary>
/// 死亡処理本体。
/// - Died イベントの発火
/// - destroyOnDeath の設定に応じて Destroy or アニメーション再生
/// </summary>
private void HandleDeath()
{
if (IsDead) return; // 二重実行防止
IsDead = true;
// まず died シグナル(イベント)を発行
Died?.Invoke();
// その後、自身の挙動(Destroy or Animation)を実行
if (destroyOnDeath)
{
// 親ごと消したい場合は、親オブジェクトにこのコンポーネントを付けておくのがおすすめです。
Destroy(gameObject);
}
else
{
// アニメーション再生モード
if (targetAnimator != null && !string.IsNullOrEmpty(deathTriggerName))
{
targetAnimator.SetTrigger(deathTriggerName);
}
else
{
// Animator が設定されていない場合はログだけ出しておく
Debug.LogWarning(
$"[HealthManager] destroyOnDeath が false ですが、Animator または deathTriggerName が設定されていません。GameObject: {gameObject.name}"
);
}
}
}
}
}
使い方の手順
ここからは、実際のシーンでの使い方を具体的に見ていきましょう。
プレイヤー・敵・動く床など、さまざまなオブジェクトに使い回せる設計になっています。
手順①:スクリプトをプロジェクトに追加
- Unity の Project ウィンドウで、
Scriptsフォルダなどを右クリック → Create > C# Script を選択。 - 名前を
HealthManagerに変更。 - 中身をすべて削除し、上記のコードをコピペして保存。
手順②:HPを持たせたいオブジェクトにアタッチ
- プレイヤーキャラクターの GameObject(例:
Player)を選択。 - Add Component ボタンから
HealthManagerを追加。 - 敵キャラクター(例:
EnemyGoblin)にも同様にHealthManagerを追加。 - 動く床や破壊可能オブジェクト(例:
BreakableCrate)にも付ければ、「HPが0になったら壊れる床」などが簡単に作れます。
インスペクター上で以下を設定します。
- Max Health:そのオブジェクトの最大HP
- Destroy On Death:
- チェック有り:HP0で
GameObjectを Destroy - チェック無し:HP0で Animator のトリガーを叩く
- チェック有り:HP0で
- Target Animator / Death Trigger Name:死亡アニメーションを使う場合のみ設定
- Enable Debug Damage:動作確認用。チェックすると Play 中に D キーでダメージを与えられます。
手順③:ダメージを与えるスクリプトから呼び出す
攻撃判定やトラップ判定などのスクリプトから、HealthManager.ApplyDamage を呼ぶだけでOKです。
例として、プレイヤーに触れたら10ダメージを与えるトゲ床のコンポーネントを作ってみます。
using UnityEngine;
using Sample; // HealthManager の namespace を使用
/// <summary>
/// プレイヤーに触れたらダメージを与えるトゲ床。
/// </summary>
[RequireComponent(typeof(Collider))]
public class SpikeTrap : MonoBehaviour
{
[SerializeField] private int damage = 10;
private void OnTriggerEnter(Collider other)
{
// 触れた相手が HealthManager を持っていればダメージを与える
var health = other.GetComponent<HealthManager>();
if (health == null) return;
health.ApplyDamage(damage);
}
}
こうしておくと、「ダメージをどう処理するか」はすべて HealthManager 側に集約されるので、
トゲ床や敵の攻撃スクリプトは「ダメージを送るだけ」のシンプルな責務になります。
手順④:死亡イベント(diedシグナル)を別コンポーネントで受け取る
スコア加算やエフェクト再生などは、別コンポーネントに分けて Died イベントを購読すると綺麗に分離できます。
例:敵が死んだときにスコアを加算するコンポーネント
using UnityEngine;
using Sample;
/// <summary>
/// HealthManager の Died イベントを監視して、死亡時にスコアを加算するコンポーネント。
/// </summary>
[RequireComponent(typeof(HealthManager))]
public class EnemyScoreOnDeath : MonoBehaviour
{
[SerializeField] private int scoreValue = 100;
private HealthManager _health;
private void Awake()
{
_health = GetComponent<HealthManager>();
}
private void OnEnable()
{
// Died イベントに購読
_health.Died += OnDied;
}
private void OnDisable()
{
// 購読解除(メモリリーク&多重呼び出し防止)
_health.Died -= OnDied;
}
private void OnDied()
{
// ここでスコアマネージャーに通知したり、静的メソッドを呼んだりする
Debug.Log($"+{scoreValue} score! ({gameObject.name} died)");
}
}
このように、「HP管理」「スコア加算」「エフェクト再生」などの責務をコンポーネント単位で分けることで、
巨大な God クラスを避けつつ、挙動を組み合わせていけるようになります。
メリットと応用
メリット①:プレハブが「HPを持つかどうか」だけで整理できる
HealthManager をプレハブに付けておくだけで、
- プレイヤー
- 敵(ザコ・ボス)
- 破壊可能オブジェクト(木箱・バリケード)
- タイム制限付きの床(HPをタイマー代わりに使う応用もアリ)
といったオブジェクトを、同じインターフェース(ApplyDamage / Heal / Died)で扱えます。
レベルデザイン時は「HPを持たせたいものにはとりあえず HealthManager を付ける」というルールにしておくと、
あとから共通の処理(例: ダメージエフェクト)を追加しやすくなります。
メリット②:死亡演出の差し替えが簡単
- 雑魚敵:
DestroyOnDeath = trueにして、即座に消す - ボス:
DestroyOnDeath = false+ Animator で長めの死亡モーションを再生 - オブジェクト:死亡時にパーティクルを出してから消す(別コンポーネントで Died を購読)
といったように、同じ HealthManager を使い回しつつ、死亡時の見た目だけを差し替えできます。
ロジックは共通、演出は個別、という理想的な分離ですね。
メリット③:テストがしやすい
デバッグ用に Dキーでダメージ を送れるようにしてあるので、
- UIのHPバーが正しく減るか
- 死亡アニメーションが正しく再生されるか
- スコア加算やSE再生が複数回発火していないか
などを、敵AIやゲーム全体の流れに依存せずにテストできます。
こういった「単体でテストできるコンポーネント」は、長期開発でじわじわ効いてきます。
改造案:一定時間後に自動Destroyする死亡処理
死亡アニメーション再生後に少し待ってから消したい、というケースも多いです。
その場合は HandleDeath 内でコルーチンを呼び出すように改造すると良いでしょう。
private void HandleDeath()
{
if (IsDead) return;
IsDead = true;
Died?.Invoke();
if (destroyOnDeath)
{
// すぐに消すのではなく、2秒待ってから Destroy
StartCoroutine(DestroyAfterDelay(2f));
}
else
{
if (targetAnimator != null && !string.IsNullOrEmpty(deathTriggerName))
{
targetAnimator.SetTrigger(deathTriggerName);
// アニメーション長に合わせて遅延Destroyするのもアリ
StartCoroutine(DestroyAfterDelay(3f));
}
}
}
private System.Collections.IEnumerator DestroyAfterDelay(float delay)
{
yield return new WaitForSeconds(delay);
Destroy(gameObject);
}
このように、小さな責務のコンポーネントにしておくと、
「死亡後に一定時間だけ残す」「リスポーン用に非アクティブにする」などのアレンジがやりやすくなります。
HP管理を HealthManager に任せてしまえば、他のコンポーネントは
「ダメージを送る」「死亡イベントを受け取る」という2つのインターフェースだけを意識すればよくなります。
ぜひプロジェクトの標準コンポーネントとして育ててみてください。
