Unityを触り始めたころは、つい「とりあえず全部Updateに書く」実装をしがちですよね。
プレイヤー入力、移動、アニメーション、HP管理、状態異常(毒・炎上・スタン…)などを1つのスクリプトに押し込んでしまうと、だんだん巨大なGodクラスになっていきます。
- どこでHPが減っているのか分からない
- 毒ダメージのロジックを変えたいだけなのに、プレイヤー制御まで巻き込んでバグる
- 敵とプレイヤーで同じ毒の仕組みを使いまわしたいのに、プレイヤー専用スクリプトに埋め込んでいて再利用できない
こうした問題は、「状態異常は状態異常のコンポーネントに分離する」ことでかなりスッキリ解決できます。
この記事では、一定時間ごとに親の HealthManager を呼び出して継続ダメージを与える「毒状態」を、PoisonEffect というコンポーネントとして切り出してみましょう。
【Unity】継続ダメージをスマートに分離!「PoisonEffect」コンポーネント
ここでは、「HPを管理するコンポーネント」と、「毒ダメージを与えるコンポーネント」を完全に分離します。
HealthManager:ダメージを受ける側のHP管理(プレイヤーでも敵でもOK)PoisonEffect:一定間隔で親のHealthManagerにダメージを通知する毒状態
この2つを組み合わせることで、プレイヤーにも敵にも、動くトゲ床にも、同じ仕組みで毒を付与できるようにします。
フルコード:HealthManager と PoisonEffect
まずはそのままコピペで動く完全なコードを載せます。
Unity6(C#)でそのまま使える形になっています。
HealthManager.cs(HP管理コンポーネント)
using UnityEngine;
/// <summary>シンプルなHP管理コンポーネント</summary>
public class HealthManager : MonoBehaviour
{
// 最大HP
[SerializeField] private float maxHealth = 100f;
// 現在HP
[SerializeField] private float currentHealth = 100f;
// 死亡したかどうか
public bool IsDead => currentHealth <= 0f;
private void Awake()
{
// シーン上で currentHealth が0以下なら最大値で初期化
if (currentHealth <= 0f)
{
currentHealth = maxHealth;
}
}
/// <summary>
/// ダメージを受ける
/// </summary>
/// <param name="amount">ダメージ量(正の値)</param>
public void ApplyDamage(float amount)
{
if (IsDead)
{
// すでに死亡しているなら何もしない
return;
}
// ダメージは正の値を想定
float damage = Mathf.Max(0f, amount);
currentHealth -= damage;
currentHealth = Mathf.Clamp(currentHealth, 0f, maxHealth);
Debug.Log($"{gameObject.name} took {damage} damage. HP: {currentHealth}/{maxHealth}");
if (IsDead)
{
OnDead();
}
}
/// <summary>
/// HPを回復する
/// </summary>
/// <param name="amount">回復量(正の値)</param>
public void Heal(float amount)
{
if (IsDead)
{
// 死亡後は回復しない実装
return;
}
float heal = Mathf.Max(0f, amount);
currentHealth += heal;
currentHealth = Mathf.Clamp(currentHealth, 0f, maxHealth);
Debug.Log($"{gameObject.name} healed {heal}. HP: {currentHealth}/{maxHealth}");
}
/// <summary>
/// 死亡時の処理
/// 実際のゲームではここでアニメーション再生やリスポーン処理などを呼ぶ
/// </summary>
private void OnDead()
{
Debug.Log($"{gameObject.name} is Dead.");
// 必要に応じてオブジェクトを無効化・破棄する
// gameObject.SetActive(false);
// Destroy(gameObject);
}
}
PoisonEffect.cs(毒状態コンポーネント)
using UnityEngine;
/// <summary>
/// 一定時間ごとに親の HealthManager に継続ダメージを与える「毒状態」コンポーネント
/// </summary>
[DisallowMultipleComponent] // 同じオブジェクトに複数付けるのを防ぐ
public class PoisonEffect : MonoBehaviour
{
[Header("毒の基本パラメータ")]
[Tooltip("毒ダメージを与える対象。未指定の場合は親階層から自動検索します")]
[SerializeField] private HealthManager targetHealth;
[Tooltip("1回あたりの毒ダメージ量")]
[SerializeField] private float damagePerTick = 5f;
[Tooltip("ダメージを与える間隔(秒)")]
[SerializeField] private float tickInterval = 1f;
[Tooltip("毒が続く合計時間(秒)")]
[SerializeField] private float duration = 5f;
[Header("挙動設定")]
[Tooltip("オブジェクト有効化時(OnEnable)に自動で毒を開始するか")]
[SerializeField] private bool autoStartOnEnable = true;
[Tooltip("毒が終了したときに、このコンポーネントを自動で破棄するか")]
[SerializeField] private bool destroyOnFinish = true;
// 内部状態
private float _elapsed; // 経過時間
private float _tickTimer; // 次のダメージまでのカウントダウン
private bool _isRunning; // 毒が有効かどうか
private void Reset()
{
// コンポーネントを追加したときに、親階層から HealthManager を自動取得
if (targetHealth == null)
{
targetHealth = GetComponentInParent<HealthManager>();
}
}
private void Awake()
{
// Awake 時点でも一応自動取得を試みる
if (targetHealth == null)
{
targetHealth = GetComponentInParent<HealthManager>();
}
}
private void OnEnable()
{
if (autoStartOnEnable)
{
StartPoison();
}
}
private void Update()
{
// 毒が動作していなければ何もしない
if (!_isRunning)
{
return;
}
// 対象が存在しない、またはすでに死亡しているなら毒を停止
if (targetHealth == null || targetHealth.IsDead)
{
StopPoison();
return;
}
float deltaTime = Time.deltaTime;
// 全体の経過時間を進める
_elapsed += deltaTime;
// 毒の合計時間を超えたら終了
if (_elapsed >= duration)
{
StopPoison();
return;
}
// 次のダメージまでのタイマーを進める
_tickTimer -= deltaTime;
// タイマーが0以下になったらダメージを与える
if (_tickTimer <= 0f)
{
ApplyPoisonDamage();
// 次のティックまでの時間をリセット
_tickTimer += tickInterval;
}
}
/// <summary>
/// 毒状態を開始する
/// </summary>
public void StartPoison()
{
if (targetHealth == null)
{
// 対象がいない場合はログを出して終了
Debug.LogWarning($"PoisonEffect on {gameObject.name} has no targetHealth assigned.");
_isRunning = false;
return;
}
// パラメータが不正な場合の保険
damagePerTick = Mathf.Max(0f, damagePerTick);
tickInterval = Mathf.Max(0.01f, tickInterval); // 0は危険なので最小値を設定
duration = Mathf.Max(0.01f, duration);
_elapsed = 0f;
_tickTimer = 0f; // すぐに1回目のダメージを与えたいので0スタート
_isRunning = true;
Debug.Log($"PoisonEffect started on {targetHealth.gameObject.name} via {gameObject.name}");
}
/// <summary>
/// 毒状態を停止する
/// </summary>
public void StopPoison()
{
if (!_isRunning)
{
return;
}
_isRunning = false;
Debug.Log($"PoisonEffect stopped on {gameObject.name}");
if (destroyOnFinish)
{
// コンポーネントだけを破棄(GameObject本体は残す)
Destroy(this);
}
}
/// <summary>
/// 実際に毒ダメージを与える処理
/// </summary>
private void ApplyPoisonDamage()
{
if (targetHealth == null)
{
return;
}
// HealthManager にダメージを通知
targetHealth.ApplyDamage(damagePerTick);
// デバッグ用ログ(実際のゲームではエフェクト再生などに置き換え)
Debug.Log($"Poison tick: {damagePerTick} damage to {targetHealth.gameObject.name}");
}
/// <summary>
/// 外部から毒パラメータを上書きして開始したい場合に使えるヘルパー
/// </summary>
public void ConfigureAndStart(HealthManager newTarget, float newDamagePerTick, float newTickInterval, float newDuration)
{
targetHealth = newTarget;
damagePerTick = newDamagePerTick;
tickInterval = newTickInterval;
duration = newDuration;
StartPoison();
}
}
使い方の手順
ここからは、実際にプレイヤーや敵に毒状態を付与する手順を見ていきます。
手順①:HPを管理したいオブジェクトに HealthManager を追加
- プレイヤー、敵、動く床など、ダメージを受ける対象の GameObject を選択します。
Add ComponentからHealthManagerを追加します。- インスペクターで
Max Health:例)100Current Health:未設定なら自動で Max と同じ値になります
これで、そのオブジェクトは ApplyDamage / Heal でHPを増減できるようになります。
手順②:同じオブジェクト or 子オブジェクトに PoisonEffect を追加
- 毒状態を管理したい GameObject を選択します。
- 基本パターン:プレイヤー本体や敵本体にそのまま追加
- 分離パターン:プレイヤーの子オブジェクト(「StatusEffects」など)を作ってそこに追加
Add ComponentからPoisonEffectを追加します。
同じ階層か親階層に HealthManager が存在する場合、Reset() と Awake() で自動的に targetHealth を探します。
見つからなかった場合は、インスペクターの Target Health に手動でドラッグ&ドロップしてください。
手順③:毒のパラメータを設定
PoisonEffect のインスペクターで、以下を調整します。
Damage Per Tick:1回あたりのダメージ量(例:5)Tick Interval:ダメージ間隔(秒)(例:1秒ごと)Duration:毒が続く合計時間(秒)(例:5秒)Auto Start On Enable:- ON:オブジェクトが有効化された瞬間に毒開始
- OFF:外部スクリプトから
StartPoison()を呼ぶまで待機
Destroy On Finish:- ON:毒終了時に
PoisonEffectコンポーネントを自動削除 - OFF:コンポーネントは残り、
StartPoison()を再度呼べる
- ON:毒終了時に
手順④:具体的な使用例
例1:敵がプレイヤーに毒を付与する
プレイヤー側:
PlayerGameObject にHealthManagerを追加
敵側:
EnemyGameObject に、プレイヤー接触時に毒を付与するスクリプトを追加します。
using UnityEngine;
/// <summary>プレイヤー接触時に毒を付与する例</summary>
public class EnemyPoisonAttacker : MonoBehaviour
{
[SerializeField] private float poisonDamagePerTick = 3f;
[SerializeField] private float poisonInterval = 1f;
[SerializeField] private float poisonDuration = 6f;
private void OnTriggerEnter(Collider other)
{
// Player タグのオブジェクトに触れたら毒を付与する例
if (!other.CompareTag("Player")) return;
// プレイヤーの HealthManager を取得
HealthManager targetHealth = other.GetComponentInParent<HealthManager>();
if (targetHealth == null) return;
// すでに PoisonEffect が付いているか確認
PoisonEffect poison = other.GetComponentInChildren<PoisonEffect>();
if (poison == null)
{
// なければプレイヤーに PoisonEffect を動的に追加
poison = other.gameObject.AddComponent<PoisonEffect>();
}
// パラメータを設定して毒を開始
poison.ConfigureAndStart(
targetHealth,
poisonDamagePerTick,
poisonInterval,
poisonDuration
);
}
}
このように、敵は「毒を付与する」責務だけを持ち、実際のHP減少ロジックは HealthManager と PoisonEffect に任せられます。
例2:毒の床(トラップ)に乗ると毒状態になる
PoisonFloorという GameObject(BoxCollider +isTrigger = true)を作成。- 以下のスクリプトを追加します。
using UnityEngine;
/// <summary>上に乗ったオブジェクトに毒を付与する床</summary>
public class PoisonFloor : MonoBehaviour
{
[SerializeField] private float poisonDamagePerTick = 2f;
[SerializeField] private float poisonInterval = 0.5f;
[SerializeField] private float poisonDuration = 4f;
private void OnTriggerEnter(Collider other)
{
HealthManager targetHealth = other.GetComponentInParent<HealthManager>();
if (targetHealth == null) return;
PoisonEffect poison = other.GetComponentInChildren<PoisonEffect>();
if (poison == null)
{
poison = other.gameObject.AddComponent<PoisonEffect>();
}
poison.ConfigureAndStart(
targetHealth,
poisonDamagePerTick,
poisonInterval,
poisonDuration
);
}
}
プレイヤーでも敵でも、HealthManager を持っていれば同じ床で毒状態にできます。
「毒の沼」「毒の霧」など、レベルデザイン側で簡単にバリエーションを増やせるのがポイントですね。
メリットと応用
メリット①:責務が分離されていて読みやすい
HealthManagerは「HPの増減と死亡判定」だけを見るPoisonEffectは「一定間隔でダメージを通知する」だけを見る
このように責務を分けておくと、毒の仕様変更(ダメージ量や間隔、重複ルールなど)を行うときも、PoisonEffect だけ読めばよくなります。
巨大な PlayerController の中から毒の処理を探す……といった地獄から解放されます。
メリット②:プレハブ化・レベルデザインが楽になる
- 「毒を持つ敵」プレハブ:「敵本体 + HealthManager + PoisonEffect」
- 「毒の床」プレハブ:「床オブジェクト + PoisonFloor」
このように、コンポーネントの組み合わせだけで挙動を作れるので、レベルデザイナーや他の開発者も扱いやすくなります。
毒のパラメータ(ダメージ量・間隔・継続時間)もインスペクターで調整できるので、ゲームバランス調整も楽ですね。
メリット③:状態異常の追加がしやすい
毒がきれいにコンポーネントとして分離されていれば、
BurnEffect(炎上)SlowEffect(移動速度低下)StunEffect(行動不能)
といった別の状態異常も、同じパターンでどんどん増やしていけます。
大事なのは、「1つのコンポーネントは1つの役割に集中させる」という考え方ですね。
改造案:毒の残り時間を取得するメソッドを追加する
UIに「毒:残り3.2秒」と表示したくなる場面も多いので、PoisonEffect に残り時間を返す関数を追加してみましょう。
/// <summary>
/// 毒の残り時間(秒)を取得する
/// 動作していない場合は 0 を返す
/// </summary>
public float GetRemainingTime()
{
if (!_isRunning)
{
return 0f;
}
return Mathf.Max(0f, duration - _elapsed);
}
この関数を使えば、別の UI 用コンポーネントから
poisonEffect.GetRemainingTime()を呼んでスライダーやテキストに反映
といった形で、表示ロジックもきれいに分離できます。
このように、必要になったタイミングで小さなメソッドを追加していくと、コンポーネント指向らしい拡張のしやすさを実感できるはずです。
