Unityを触り始めた頃、「とりあえず Update() に全部書いておけば動くし楽!」となりがちですよね。
プレイヤーの移動、攻撃、被ダメージ判定、UI更新…すべてを1つの巨大スクリプトに詰め込んでしまうと、次のような問題がすぐに出てきます。
- どこを直せばいいか分からない(バグ調査がつらい)
- 機能追加のたびに1ファイルがどんどん肥大化する
- 敵やギミックにも流用しづらく、コピペ地獄になる
そこでおすすめなのが、「攻撃判定」「被ダメージ判定」「体力管理」などを、それぞれ小さなコンポーネントに分割していく設計です。
この記事では、その中でも「被ダメージ判定」に絞った HurtboxComponent を作り、敵の攻撃(Hitbox)と重なったときに HealthManager のHPを減らす仕組みをコンポーネントとして切り出してみます。
【Unity】当たり判定をコンポーネント化!「HurtboxComponent」コンポーネント
ここでは 2D アクションゲームを想定し、トリガー付きの2Dコライダー(Area的な領域) を「Hurtbox」として扱います。
敵側は「Hitbox」コライダーを持っており、両者が重なった瞬間にダメージを受ける、という構造ですね。
ポイントは次の3つです。
HealthManagerは「体力管理」だけを担当するシンプルなコンポーネントHurtboxComponentは「何と当たったらどれだけ減らすか」だけを担当- 敵側の「Hitbox」はタグや専用コンポーネントで識別し、柔軟に拡張できるようにする
それでは、コピペで動く完全なコードを順番に見ていきましょう。
HealthManager(体力管理)コンポーネント
using UnityEngine;
namespace Sample
{
/// <summary>
/// シンプルなHP管理コンポーネント
/// HurtboxComponent からダメージを受け取る想定
/// </summary>
public class HealthManager : MonoBehaviour
{
[Header("HP設定")]
[SerializeField] private int maxHealth = 100; // 最大HP
[SerializeField] private int currentHealth = 100; // 現在HP(インスペクタで調整可)
[Header("死亡時にオブジェクトを破壊するか")]
[SerializeField] private bool destroyOnDeath = true;
/// <summary>現在のHP(読み取り専用)</summary>
public int CurrentHealth => currentHealth;
private void Awake()
{
// currentHealth が 0 なら maxHealth で初期化しておく
if (currentHealth <= 0)
{
currentHealth = maxHealth;
}
}
/// <summary>
/// ダメージを受ける(負の値は回復として扱ってもOK)
/// </summary>
/// <param name="amount">ダメージ量(正の値で減少)</param>
public void ApplyDamage(int amount)
{
if (amount == 0)
{
return;
}
currentHealth -= amount;
currentHealth = Mathf.Clamp(currentHealth, 0, maxHealth);
Debug.Log($"[{name}] took damage: {amount}, HP: {currentHealth}/{maxHealth}");
if (currentHealth <= 0)
{
OnDeath();
}
}
/// <summary>
/// 死亡時の処理
/// ゲームによってはアニメーションやリスポーン処理に差し替える
/// </summary>
protected virtual void OnDeath()
{
Debug.Log($"[{name}] is dead.");
if (destroyOnDeath)
{
Destroy(gameObject);
}
}
}
}
EnemyHitbox(攻撃判定の情報を持つコンポーネント)
敵の攻撃判定(Hitbox)側に、「この攻撃はいくらダメージを与えるのか?」という情報を持たせるコンポーネントです。
これを付けたオブジェクトの 2D コライダーを「Hitbox」とみなし、Hurtbox と重なったときに参照します。
using UnityEngine;
namespace Sample
{
/// <summary>
/// 敵の攻撃判定(Hitbox)に付けるコンポーネント
/// ダメージ量などを保持する
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class EnemyHitbox : MonoBehaviour
{
[Header("ダメージ設定")]
[SerializeField] private int damage = 10; // このヒットボックスが与えるダメージ
[Header("一度の接触で一回だけダメージを与えるか")]
[SerializeField] private bool damageOncePerTouch = true;
// Hurtbox側で「同じヒットボックスからの連続ダメージ」を制御したい場合に使う
public int Damage => damage;
public bool DamageOncePerTouch => damageOncePerTouch;
}
}
HurtboxComponent(被ダメージ判定)コンポーネント
メインの主役です。
2D のトリガーコライダー(いわゆる Area2D 的な領域)を持ち、敵の Hitbox(EnemyHitbox コンポーネント付きオブジェクト)と重なったら HealthManager にダメージを渡します。
using System.Collections.Generic;
using UnityEngine;
namespace Sample
{
/// <summary>
/// 被ダメージ判定を担当するコンポーネント
/// 2Dトリガーコライダーを「Hurtbox」として扱い、
/// 敵の Hitbox (EnemyHitbox) と重なったら HealthManager にダメージを送る
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class HurtboxComponent : MonoBehaviour
{
[Header("参照コンポーネント")]
[SerializeField] private HealthManager healthManager; // ダメージを適用する対象
[Header("Hitbox識別設定")]
[Tooltip("EnemyHitbox コンポーネントを探す前に、タグでフィルタしたい場合に指定")]
[SerializeField] private string hitboxTag = "EnemyHitbox";
[Header("デバッグ設定")]
[SerializeField] private bool showDebugLog = true;
// 「一度の接触で一回だけダメージを与える」Hitbox用に、
// すでにダメージを与えた EnemyHitbox を管理する
private readonly HashSet<EnemyHitbox> _alreadyDamagedHitboxes = new HashSet<EnemyHitbox>();
private Collider2D _collider2D;
private void Reset()
{
// コンポーネント追加時に、トリガー設定を自動でONにしておく
_collider2D = GetComponent<Collider2D>();
if (_collider2D != null)
{
_collider2D.isTrigger = true;
}
// 同じオブジェクト上の HealthManager を自動参照
if (healthManager == null)
{
healthManager = GetComponent<HealthManager>();
}
}
private void Awake()
{
_collider2D = GetComponent<Collider2D>();
if (_collider2D != null && !_collider2D.isTrigger)
{
Debug.LogWarning($"[{name}] HurtboxComponent の Collider2D は isTrigger = true を推奨します。自動で変更しました。");
_collider2D.isTrigger = true;
}
if (healthManager == null)
{
// 同じオブジェクト上に HealthManager があれば自動取得
healthManager = GetComponent<HealthManager>();
}
if (healthManager == null)
{
Debug.LogError($"[{name}] HurtboxComponent に HealthManager の参照が設定されていません。");
}
}
private void OnTriggerEnter2D(Collider2D other)
{
// タグ指定がある場合は、まずタグでフィルタ
if (!string.IsNullOrEmpty(hitboxTag) && !other.CompareTag(hitboxTag))
{
return;
}
// EnemyHitbox コンポーネントを探す
if (!other.TryGetComponent(out EnemyHitbox enemyHitbox))
{
return;
}
// すでに一度ダメージを与えていて、かつ
// 「一度の接触で一回だけダメージ」設定ならスキップ
if (enemyHitbox.DamageOncePerTouch && _alreadyDamagedHitboxes.Contains(enemyHitbox))
{
return;
}
ApplyDamageFromHitbox(enemyHitbox);
}
private void OnTriggerExit2D(Collider2D other)
{
// 接触が終わったら、ダメージ済みリストから削除して
// 再度触れた時にダメージを与えられるようにする
if (!other.TryGetComponent(out EnemyHitbox enemyHitbox))
{
return;
}
if (_alreadyDamagedHitboxes.Contains(enemyHitbox))
{
_alreadyDamagedHitboxes.Remove(enemyHitbox);
}
}
/// <summary>
/// EnemyHitbox からダメージを適用する
/// </summary>
/// <param name="enemyHitbox">接触した EnemyHitbox</param>
private void ApplyDamageFromHitbox(EnemyHitbox enemyHitbox)
{
if (healthManager == null)
{
Debug.LogWarning($"[{name}] HealthManager が設定されていないため、ダメージを適用できません。");
return;
}
int damage = Mathf.Max(0, enemyHitbox.Damage);
if (damage <= 0)
{
// 0ダメージなら何もしない
return;
}
healthManager.ApplyDamage(damage);
if (enemyHitbox.DamageOncePerTouch)
{
_alreadyDamagedHitboxes.Add(enemyHitbox);
}
if (showDebugLog)
{
Debug.Log($"[{name}] が {enemyHitbox.name} から {damage} ダメージを受けました。");
}
}
}
}
使い方の手順
ここからは、実際にプレイヤーと敵を例にして、このコンポーネント群をどう使うかを手順で見ていきましょう。
手順① プレイヤーに HealthManager と HurtboxComponent を付ける
- シーン内のプレイヤー GameObject を選択します(例:
Player)。 - Add Component から
HealthManagerを追加します。
・Max HealthやCurrent Healthを好きな値(例: 100)に設定します。 - 同じく Add Component から
HurtboxComponentを追加します。 - プレイヤーに Collider2D(例:
CircleCollider2DやBoxCollider2D)を追加し、Is Trigger にチェックを入れます。
※HurtboxComponentが自動でトリガーにしてくれますが、見た目としてもONにしておくと安心です。 HurtboxComponentのHealth Managerフィールドに、同じオブジェクトのHealthManagerをドラッグ&ドロップして設定します(または自動で入っていればOK)。- 必要であれば
Hitbox Tagに「EnemyHitbox」などのタグ名を設定します(後で敵側に同じタグを付けます)。
手順② 敵に Hitbox(EnemyHitbox)を用意する
- シーン内の敵 GameObject を選択します(例:
Enemy)。 - 敵の「攻撃判定」となる子オブジェクトを作成します(例:
EnemyHitboxArea)。
・Enemyを右クリック → Create Empty → 名前をEnemyHitboxAreaに変更。 EnemyHitboxAreaに Collider2D を追加し、Is Trigger にチェックを入れます。
・プレイヤーに当たる範囲に合わせてサイズを調整します。EnemyHitboxAreaにEnemyHitboxコンポーネントを追加し、Damageを 10 や 20 など任意の値に設定します。- タグによるフィルタを使う場合は、
EnemyHitboxAreaに EnemyHitbox というタグを作成・設定します。
・Unity 上部メニューの Tags & Layers から新しいタグを作り、EnemyHitboxAreaに割り当ててください。
手順③ プレイヤーと敵を配置してテストする
- シーン内でプレイヤーと敵の位置を近づけ、敵の Hitbox(
EnemyHitboxAreaのコライダー)がプレイヤーの Hurtbox(プレイヤーの 2D コライダー)と重なるように配置します。 - ゲームを再生し、コンソールログを確認します。
・接触した瞬間に[Player] took damage: 10, HP: 90/100のようなログが出れば成功です。 - 敵側の
EnemyHitboxのDamage Once Per Touchを ON にしている場合、
・1回触れたあと、そのまま触れ続けてもダメージが増えないことを確認してみましょう。
手順④ 応用例:動く床やトラップにも Hurtbox を使う
この仕組みはプレイヤー専用ではありません。
例えば、次のようなオブジェクトにも HurtboxComponent を付けるだけで、同じロジックを再利用できます。
- 動く床:床に HP を持たせて、敵の攻撃で壊れるギミック
- 防御壁:壁にも HP を持たせて、一定ダメージで破壊
- 味方NPC:プレイヤーと同じ
HealthManager + HurtboxComponent構成で被ダメージを共有
どのオブジェクトも「HP管理」と「被ダメージ判定」のコンポーネントを付けるだけで済むので、プレハブ化して量産するのがとても楽になります。
メリットと応用
コンポーネント分割のメリット
- 責務が明確:
HealthManagerは「HPの増減と死亡処理」だけ、HurtboxComponentは「何と当たったらダメージを渡すか」だけを担当します。 - 再利用性が高い:プレイヤー、敵、ギミックなど、どのオブジェクトにも同じコンポーネントを付けるだけで被ダメージ機能を共有できます。
- プレハブ管理が楽:HPやダメージ量はインスペクタから変更できるため、レベルデザイナーがコードを触らずにバランス調整できます。
- テストしやすい:HPだけをテストしたいときは
HealthManagerだけを見る、当たり判定だけを確認したいときはHurtboxComponentだけを見る、という切り分けができます。
改造案:無敵時間(i-frames)を追加する
よくある拡張として、「ダメージを受けた直後は一定時間ダメージを受け付けない(無敵時間)」という仕様があります。
これは HurtboxComponent に少しコードを足すだけで実現できます。
例として、HurtboxComponent に次のようなメソッドとフィールドを追加してみましょう。
[Header("無敵時間設定")]
[SerializeField] private float invincibleDuration = 0.5f; // ダメージ後の無敵時間(秒)
private float _invincibleUntilTime = 0f;
/// <summary>
/// 現在無敵かどうか
/// </summary>
private bool IsInvincible()
{
return Time.time < _invincibleUntilTime;
}
// ApplyDamageFromHitbox の先頭に以下を追加
private void ApplyDamageFromHitbox(EnemyHitbox enemyHitbox)
{
if (IsInvincible())
{
// 無敵時間中はダメージを受けない
return;
}
// ここから既存処理...
// healthManager.ApplyDamage(damage); の直後に
_invincibleUntilTime = Time.time + invincibleDuration;
}
これにより、「連続ヒットで一瞬でHPが溶ける」問題を簡単に防げます。
さらに、無敵中はスプライトの点滅やシェーダー変更を行うコンポーネントを別途用意すれば、視覚的なフィードバックもきれいに分離して実装できます。
このように、小さなコンポーネントに責務を分けておくと、後からの機能追加や改造がとてもやりやすくなります。
巨大な Update() にすべてを書き込む前に、「これは1つのコンポーネントに切り出せないかな?」と考えるクセをつけていきましょう。
