Unityのアクションゲームを作り始めると、つい「攻撃処理を全部PlayerのUpdateに書く」という実装をしがちですよね。
- ボタン入力を見て
- アニメーションを切り替えて
- 敵との当たり判定を見て
- HPを減らしてノックバックさせて…
これらを1つのスクリプトのUpdateに詰め込むと、すぐに1,000行級のGodクラスになってしまいます。
攻撃判定の形を変えたいだけなのに、プレイヤーのコードを開いてゴリゴリ書き換える…というのは避けたいところですね。
そこでこの記事では、攻撃判定だけを独立させた小さなコンポーネントとして、
「HitboxComponent」を用意して、Hurtbox(被弾判定)にダメージ情報を送る仕組みを作っていきます。
【Unity】攻撃判定をコンポーネント化!「HitboxComponent」コンポーネント
ここでは以下のような設計にします。
- HitboxComponent … 攻撃判定。攻撃側のコンポーネント。
- HurtboxComponent … 被弾判定。ダメージを受ける側のコンポーネント。
- DamageData … ダメージ量やノックバックなどの情報をまとめた構造体。
Unityには「Area2D」はありませんが、2D物理ではCollider2D + Rigidbody2Dで同等のことができます。
この記事では、「トリガー付きのCollider2Dを使った攻撃判定」として実装します。
フルコード(Hitbox / Hurtbox / DamageData)
using UnityEngine;
namespace HitboxSample
{
/// <summary>
/// ダメージに関する情報をまとめた構造体
/// 攻撃力だけでなく、ノックバック方向やヒットストップ時間などもここに追加できます。
/// </summary>
[System.Serializable]
public struct DamageData
{
// 与えるダメージ量
public int damageAmount;
// ノックバック方向(正規化ベクトルを想定)
public Vector2 knockbackDirection;
// ノックバックの強さ
public float knockbackForce;
// ヒットストップ時間(秒)
public float hitStopDuration;
public DamageData(int damageAmount, Vector2 knockbackDirection, float knockbackForce, float hitStopDuration)
{
this.damageAmount = damageAmount;
this.knockbackDirection = knockbackDirection.normalized;
this.knockbackForce = knockbackForce;
this.hitStopDuration = hitStopDuration;
}
}
/// <summary>
/// 被弾判定(Hurtbox)。
/// HitboxComponent からダメージ通知を受け取り、HPを減らすなどの処理を行います。
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class HurtboxComponent : MonoBehaviour
{
[Header("耐久力 / HP")]
[SerializeField] private int maxHealth = 10;
[SerializeField] private int currentHealth;
// ノックバックに使うRigidbody2D(任意)
[Header("ノックバック用 (任意)")]
[SerializeField] private Rigidbody2D targetRigidbody;
private void Awake()
{
// currentHealth が 0 以下なら初期化時に maxHealth を代入
if (currentHealth <= 0)
{
currentHealth = maxHealth;
}
// Rigidbody2D がアタッチされていれば自動取得
if (targetRigidbody == null)
{
targetRigidbody = GetComponentInParent<Rigidbody2D>();
}
}
/// <summary>
/// Hitbox から呼ばれるダメージ処理の入口。
/// ここで HP を減らしたり、ノックバックさせたりします。
/// </summary>
/// <param name="damageData">ダメージ情報</param>
/// <param name="source">攻撃元の GameObject (プレイヤーや敵など)</param>
public void ApplyDamage(DamageData damageData, GameObject source)
{
// HP 減少
currentHealth -= damageData.damageAmount;
Debug.Log($"{gameObject.name} が {source.name} から {damageData.damageAmount} ダメージを受けた! 残りHP: {currentHealth}");
// ノックバック処理(Rigidbody2D がある場合のみ)
if (targetRigidbody != null && damageData.knockbackForce > 0f)
{
targetRigidbody.AddForce(damageData.knockbackDirection.normalized * damageData.knockbackForce, ForceMode2D.Impulse);
}
// ヒットストップなどの演出はここに追加可能
if (damageData.hitStopDuration > 0f)
{
// 実際のゲームでは、Time.timeScale を一時的に下げるなどの処理を
// 別コンポーネントに委譲するのがおすすめです。
// ここではログだけにしておきます。
Debug.Log($"HitStop: {damageData.hitStopDuration} 秒 (ダミー処理)");
}
// HP が 0 以下になったら死亡処理
if (currentHealth <= 0)
{
OnDead();
}
}
/// <summary>
/// 死亡時の処理。
/// 実案件ではアニメーション再生やリスポーン処理などに分割すると良いです。
/// </summary>
private void OnDead()
{
Debug.Log($"{gameObject.name} は撃破された!");
// ここではシンプルに非アクティブ化
gameObject.SetActive(false);
}
}
/// <summary>
/// 攻撃判定(Hitbox)。
/// トリガー付き Collider2D を使って、重なった HurtboxComponent に DamageData を送ります。
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class HitboxComponent : MonoBehaviour
{
[Header("ダメージ設定")]
[SerializeField] private int damageAmount = 1;
[Tooltip("ノックバックの方向。例: (1, 0) なら右方向に吹き飛ばす")]
[SerializeField] private Vector2 knockbackDirection = Vector2.right;
[SerializeField] private float knockbackForce = 5f;
[Tooltip("ヒットストップ時間 (秒)。0 なら無し")]
[SerializeField] private float hitStopDuration = 0.05f;
[Header("有効時間制御")]
[Tooltip("開始時に自動で無効化しておくかどうか")]
[SerializeField] private bool disableOnStart = true;
[Tooltip("攻撃有効時間 (秒)。0以下なら自動無効化しない")]
[SerializeField] private float activeDuration = 0.1f;
// すでにダメージを与えた Hurtbox を記録して、1回の攻撃で多重ヒットしないようにする
private readonly System.Collections.Generic.HashSet<HurtboxComponent> _alreadyHitHurtboxes
= new System.Collections.Generic.HashSet<HurtboxComponent>();
// 有効かどうか(Collider2D 自体の OnTrigger を使うためのフラグ)
private bool _isActive;
// キャッシュしておくと毎フレーム GetComponent しなくて済む
private Collider2D _collider2D;
private void Awake()
{
_collider2D = GetComponent<Collider2D>();
// トリガーであることを強制
if (_collider2D != null && !_collider2D.isTrigger)
{
_collider2D.isTrigger = true;
}
}
private void Start()
{
if (disableOnStart)
{
SetActive(false);
}
}
/// <summary>
/// Hitbox を有効 / 無効にする。
/// 主にアニメーションイベントや攻撃開始処理から呼び出します。
/// </summary>
public void SetActive(bool active)
{
_isActive = active;
// 物理判定自体を切り替えたい場合は Collider2D.enabled も変更
if (_collider2D != null)
{
_collider2D.enabled = active;
}
if (active)
{
// 有効化時に、すでにヒットした Hurtbox のリストをクリア
_alreadyHitHurtboxes.Clear();
// 有効時間が設定されていれば、自動で無効化するコルーチンを開始
if (activeDuration > 0f)
{
StopAllCoroutines();
StartCoroutine(AutoDisableCoroutine(activeDuration));
}
}
else
{
// 無効化時はコルーチン停止
StopAllCoroutines();
}
}
/// <summary>
/// 攻撃を一時的に有効化するユーティリティ関数。
/// 例: PlayerAttack コンポーネントから呼び出す想定。
/// </summary>
public void ActivateOnce()
{
SetActive(true);
}
/// <summary>
/// 一定時間後に自動で Hitbox を無効化するコルーチン。
/// </summary>
private System.Collections.IEnumerator AutoDisableCoroutine(float duration)
{
yield return new WaitForSeconds(duration);
SetActive(false);
}
/// <summary>
/// OnTriggerEnter2D は、Collider2D が「isTrigger = true」のときに呼ばれます。
/// 相手に HurtboxComponent が付いていれば、ダメージを送ります。
/// </summary>
/// <param name="other">衝突した Collider2D</param>
private void OnTriggerEnter2D(Collider2D other)
{
if (!_isActive)
{
// 無効化中は何もしない
return;
}
// 相手の HurtboxComponent を探す(同じ GameObject または親に付いている想定)
HurtboxComponent hurtbox = other.GetComponent<HurtboxComponent>();
if (hurtbox == null)
{
hurtbox = other.GetComponentInParent<HurtboxComponent>();
}
if (hurtbox == null)
{
// Hurtbox が無ければダメージ対象ではない
return;
}
// すでにダメージを与えた Hurtbox ならスキップ(多段ヒット防止)
if (_alreadyHitHurtboxes.Contains(hurtbox))
{
return;
}
_alreadyHitHurtboxes.Add(hurtbox);
// DamageData を構築
Vector2 finalKnockbackDir = knockbackDirection;
// 攻撃元と被弾者の位置関係からノックバック方向を反転させたい場合はここで調整
// 例: X座標を比較して左右を決めるなど。
// ここでは「ローカルX正方向を基準にする」前提のままにしておきます。
DamageData damageData = new DamageData(
damageAmount,
finalKnockbackDir,
knockbackForce,
hitStopDuration
);
// 相手の Hurtbox にダメージを通知
hurtbox.ApplyDamage(damageData, gameObject);
}
/// <summary>
/// Scene上で視覚的に範囲を確認するための Gizmo 描画。
/// 開発時のみ有効です。
/// </summary>
private void OnDrawGizmosSelected()
{
if (_collider2D == null)
{
_collider2D = GetComponent<Collider2D>();
}
if (_collider2D == null)
{
return;
}
Gizmos.color = _isActive ? Color.red : Color.yellow;
// 代表的なCollider2Dごとに描画方法を変える
if (_collider2D is BoxCollider2D box)
{
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.DrawWireCube(box.offset, box.size);
}
else if (_collider2D is CircleCollider2D circle)
{
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.DrawWireSphere(circle.offset, circle.radius);
}
else if (_collider2D is CapsuleCollider2D capsule)
{
Gizmos.matrix = transform.localToWorldMatrix;
// 簡易的に Box で代用表示
Gizmos.DrawWireCube(capsule.offset, capsule.size);
}
}
}
}
使い方の手順
ここからは、実際にプレイヤーの近接攻撃を例にして使い方を説明します。
① Hurtbox をダメージを受ける側に付ける
- 敵キャラクターのプレハブ(Enemyなど)を開く
- 敵の本体オブジェクト、またはその子オブジェクトに
Collider2D(例: BoxCollider2D)を追加 - Collider2D の Is Trigger は「OFF」のままでOK(物理衝突用と兼用可能)
- 同じオブジェクトに
HurtboxComponentをアタッチ - HPを調整したい場合は、Max Health をインスペクタから変更
- ノックバックさせたい場合は、敵本体に
Rigidbody2Dを付けておきましょう
これで敵側の「被弾判定(Hurtbox)」の準備は完了です。
② Hitbox を攻撃する側に付ける(プレイヤーの近接攻撃)
- プレイヤーのプレハブ(Playerなど)を開く
- 子オブジェクトとして
Hitboxなどの名前で空のGameObjectを作成 - Hitboxオブジェクトに
BoxCollider2Dを追加し、Is Trigger にチェック - 同じオブジェクトに
HitboxComponentをアタッチ - インスペクタで以下を設定
- Damage Amount:与えたいダメージ量(例: 3)
- Knockback Direction:ローカル座標での吹き飛ばし方向(例: (1, 0) で右)
- Knockback Force:吹き飛ばしの強さ
- Hit Stop Duration:ヒットストップ時間(任意)
- Disable On Start:開始時に無効化したいので「ON」を推奨
- Active Duration:攻撃判定の有効時間(例: 0.1秒)
- BoxCollider2D の Size / Offset を調整して、攻撃のリーチに合わせる
これで「当たり判定の範囲だけ」を子オブジェクトとして独立させることができました。
レベルデザイン時には、このHitboxオブジェクトを動かすだけでリーチ調整ができるのでとても楽になります。
③ 攻撃ボタンから Hitbox を有効化する
あとは、プレイヤーの攻撃入力に合わせて HitboxComponent.ActivateOnce() を呼び出すだけです。
例として、非常にシンプルな攻撃入力用コンポーネントを示します。
using UnityEngine;
using UnityEngine.InputSystem;
using HitboxSample;
public class PlayerAttack : MonoBehaviour
{
[Header("参照")]
[SerializeField] private HitboxComponent hitbox;
// 新Input System用のアクション
[Header("Input Actions")]
[SerializeField] private InputActionReference attackAction;
private void OnEnable()
{
if (attackAction != null)
{
attackAction.action.performed += OnAttackPerformed;
attackAction.action.Enable();
}
}
private void OnDisable()
{
if (attackAction != null)
{
attackAction.action.performed -= OnAttackPerformed;
attackAction.action.Disable();
}
}
private void OnAttackPerformed(InputAction.CallbackContext context)
{
if (hitbox != null)
{
// 攻撃ボタンが押されたら Hitbox を一時的に有効化
hitbox.ActivateOnce();
}
}
}
このように、入力処理(PlayerAttack) と 攻撃判定(HitboxComponent) を分離しておくと、
- 敵キャラの攻撃でも同じ HitboxComponent を使い回せる
- 攻撃パターンごとに Hitbox オブジェクトを複数用意し、切り替えるだけでOK
- アニメーションイベントから
SetActive(true/false)を呼ぶだけで、判定フレームを細かく制御できる
といったメリットがあります。
④ 動く床やトラップにも適用してみる
HitboxComponent は「攻撃」という名前ですが、ダメージを与えるトリガー全般に使えます。
- トゲトゲの床(常時有効なHitbox)
- 一定間隔で上下する炎の柱(アニメーションに合わせて有効/無効を切り替え)
- スクロールするレーザー(移動するHitbox)
これらはすべて、トラップのオブジェクトに HitboxComponent + Collider2D (isTrigger=true) を付けるだけで実現できます。
ダメージ量やノックバック方向を変えれば、同じ仕組みで多彩なギミックを作れます。
メリットと応用
プレハブ管理・レベルデザインが楽になるポイント
- 攻撃判定の再利用性
HitboxComponent は攻撃元に依存しない設計にしているので、プレイヤー・敵・トラップなど、
どのプレハブにも同じコンポーネントを貼って使い回せます。 - 当たり判定の調整が視覚的
BoxCollider2D や CircleCollider2D のサイズ・位置を変えるだけで、攻撃のリーチや範囲を調整できます。
スクリプトを開かずに、レベルデザイナーがインスペクタだけで調整できるのが大きなメリットですね。 - 責務の分離
入力は PlayerAttack、移動は PlayerMovement、攻撃判定は HitboxComponent、被弾処理は HurtboxComponent…と、
それぞれを小さなコンポーネントに分けることで、Godクラス化を防げます。 - テストがしやすい
HitboxComponent 単体でシーンに置いて、Gizmos で範囲を確認しながら動作テストができます。
HurtboxComponent も、単純なダミー敵プレハブに付けておくだけで挙動確認が可能です。
改造案:チーム攻撃・味方への誤爆防止
応用として、「プレイヤーの攻撃は敵だけに当たる」「敵同士ではダメージを与えない」など、
チームごとの当たり判定のフィルタリングを入れたくなることが多いです。
その場合、HitboxComponent に「攻撃側のチーム種別」を追加し、HurtboxComponent に「受ける側のチーム種別」を持たせ、
一致した場合だけダメージを通すようにできます。
例えば、HurtboxComponent に以下のような関数を追加するだけでも、簡易的なフィルタリングが可能です。
public enum TeamType
{
Player,
Enemy,
Neutral
}
public class HurtboxComponent : MonoBehaviour
{
[Header("チーム設定")]
[SerializeField] private TeamType team = TeamType.Enemy;
// 既存のフィールド / メソッドは省略...
/// <summary>
/// この Hurtbox が、指定されたチームからの攻撃を受けてよいか判定する。
/// ここを差し替えるだけで、味方への誤爆を防ぐロジックを切り替えられます。
/// </summary>
public bool CanReceiveDamageFrom(TeamType attackerTeam)
{
// 例:
// - Player は Enemy の攻撃だけ受ける
// - Enemy は Player の攻撃だけ受ける
// - Neutral は誰からもダメージを受ける
if (team == TeamType.Neutral)
{
return true;
}
if (team == TeamType.Player && attackerTeam == TeamType.Enemy)
{
return true;
}
if (team == TeamType.Enemy && attackerTeam == TeamType.Player)
{
return true;
}
return false;
}
}
HitboxComponent 側で attackerTeam を持たせ、CanReceiveDamageFrom を呼んでから ApplyDamage するようにすれば、
「プレイヤーの攻撃がプレイヤー自身に当たらない」「敵同士の誤爆を防ぐ」といったルールを簡単に実装できます。
このように、攻撃判定(Hitbox)と被弾判定(Hurtbox)をコンポーネントとして分離しておくと、
ゲームの仕様変更や拡張にも柔軟に対応できるようになります。
ぜひプロジェクトの初期段階から、攻撃まわりをコンポーネント指向で設計してみてください。
