【Cocos Creator 3.8】HitboxComponent の実装:アタッチするだけで「攻撃判定 → Hurtbox へのダメージ通知」を実現する汎用スクリプト
この記事では、Cocos Creator 3.8.7 + TypeScript で使える「攻撃判定用コンポーネント HitboxComponent」を実装します。
ノードにアタッチしておくだけで、そのノードの 2D 当たり判定領域(Collider2D)に重なった Hurtbox(被弾判定)へ「ダメージ情報」を通知することができます。
このコンポーネントは以下のような場面で役立ちます。
- プレイヤーや敵キャラの近接攻撃判定
- 飛び道具やトラップ(棘床・爆発など)のダメージ判定
- ダメージ量やノックバック方向などを Inspector から手軽に調整したい場合
外部の GameManager やシングルトンには一切依存せず、HitboxComponent 単体で完結するように設計します。
ダメージを受ける側(Hurtbox)は「特定のインターフェース(メソッド名)」を持つだけの簡単な実装でよく、ここでは receiveDamage というメソッドを呼び出す形にします。
コンポーネントの設計方針
1. 機能要件の整理
- Hitbox を持つノードに 2D の Collider(BoxCollider2D / CircleCollider2D など)をアタッチして使う。
- Collider2D の トリガー(sensor) として扱い、他の Collider2D と重なったときに「攻撃判定が当たった」とみなす。
- 重なった相手に「ダメージ情報」を送る。
- ダメージを受ける側は、任意のコンポーネントに
receiveDamage(damage: DamageInfo)というメソッドを生やしておくだけでよい。 - Hitbox は「一度だけ当たる」「一定クールタイムごとに当たる」「常時当たり判定」などを Inspector から切り替えられる。
- 外部のシングルトンや GameManager には依存せず、HitboxComponent 単体で完結する。
2. Hurtbox とのインターフェース設計
Hurtbox 側には以下のようなメソッドが生えていることを前提にします。
receiveDamage(damage: {
amount: number;
hitboxNode: Node;
knockback?: Vec2;
hitType?: string;
}): void;
TypeScript 的には「インターフェース」として定義してもよいですが、HitboxComponent 自体は そのインターフェースを import しません。
「特定のメソッド名があれば呼ぶ」 という動的な呼び出しにすることで、外部依存を完全になくします。
Hitbox 側では、衝突したノードのコンポーネントを総当たりし、receiveDamage というメソッドを持つものがあれば、それを Hurtbox と見なして呼び出します。
3. インスペクタで設定可能なプロパティ
HitboxComponent に用意するプロパティと役割は以下の通りです。
enabledHitbox: boolean
– 攻撃判定の ON/OFF 切り替え。
– false のときは一切の判定処理を行わない。damageAmount: number
– 基本ダメージ量。
– 例: 10, 25 など。0 以下に設定した場合は「ダメージなし」として無視する。hitType: string
– 攻撃種別を文字列で指定。
– 例:"melee","fire","explosion"など。knockbackPower: number
– ノックバックの強さ。
– 0 の場合はノックバック情報を送らない。
– ノックバック方向は「Hitbox → Hurtbox」方向のベクトルを正規化して使用。hitOnce: boolean
– true: 一度でも当たった Hurtbox には、以後この Hitbox からはダメージを送らない。
– false: 複数回当たることを許可(クールタイム制御はhitIntervalを使用)。hitInterval: number
– 同じ Hurtbox に対してダメージを再度与えるまでの最小間隔(秒)。
– 0 の場合はクールタイムなし(当たるたびに毎フレームでもダメージが入る可能性がある)。
– 例: 0.2 秒など。maxTargetsPerFrame: number
– 1 フレームでダメージを送る最大ターゲット数。
– 0 以下の場合は無制限。debugLog: boolean
– true のとき、ダメージ送信やエラー内容をconsole.log/console.warnで出力し、挙動を確認しやすくする。
4. 必須コンポーネントと防御的実装
HitboxComponent は、同じノードに以下のコンポーネントが存在することを前提にします。
Collider2D(BoxCollider2D/CircleCollider2D/PolygonCollider2Dなど)
しかし、付いていない場合でもエラーで落ちないように、onLoad 内で getComponent(Collider2D) を行い、見つからなければ console.error を出して自動的に機能を無効化します。
また、Collider2D の トリガー(sensor)モード を前提にするため、onLoad で collider.sensor = true; を強制します(攻撃判定なので物理的な押し出しは不要なケースがほとんどのため)。
TypeScriptコードの実装
以下が完成した HitboxComponent.ts のコードです。
import { _decorator, Component, Node, Vec2, Vec3, Collider2D, Contact2DType, IPhysics2DContact, director } from 'cc';
const { ccclass, property } = _decorator;
export interface DamageInfo {
amount: number;
hitboxNode: Node;
knockback?: Vec2;
hitType?: string;
}
/**
* Hurtbox 側で実装してほしいインターフェースの形(import は不要)
*
* interface IHurtbox {
* receiveDamage(damage: DamageInfo): void;
* }
*/
@ccclass('HitboxComponent')
export class HitboxComponent extends Component {
@property({
tooltip: '攻撃判定を有効にするかどうか。\nオフの場合、Collider2D の接触イベントは処理されません。',
})
public enabledHitbox: boolean = true;
@property({
tooltip: '与える基本ダメージ量。\n0 以下の場合、この Hitbox からはダメージが発生しません。',
})
public damageAmount: number = 10;
@property({
tooltip: '攻撃種別を文字列で指定します。\n例: "melee", "fire", "explosion" など。\nHurtbox 側での分岐に利用できます。',
})
public hitType: string = 'melee';
@property({
tooltip: 'ノックバックの強さ。\n0 の場合、ノックバック情報は送信されません。\n方向は Hitbox → Hurtbox 方向ベクトルを使用します。',
})
public knockbackPower: number = 0;
@property({
tooltip: 'true の場合、一度でもヒットした Hurtbox には二度とダメージを与えません。\nfalse の場合、hitInterval ごとに再度ダメージを与えることができます。',
})
public hitOnce: boolean = false;
@property({
tooltip: '同じ Hurtbox に再度ダメージを与えるまでの最小間隔(秒)。\n0 の場合はクールタイムなし。\n例: 0.2 で 0.2 秒ごとにダメージ判定。',
min: 0,
})
public hitInterval: number = 0;
@property({
tooltip: '1 フレームでダメージを送る最大ターゲット数。\n0 以下の場合は無制限。',
min: 0,
})
public maxTargetsPerFrame: number = 0;
@property({
tooltip: 'true の場合、ダメージ送信やエラー情報をログ出力します。\nデバッグ時のみ有効にすると便利です。',
})
public debugLog: boolean = false;
private _collider: Collider2D | null = null;
// すでにヒットした Hurtbox ノードを記録(hitOnce 用)
private _alreadyHitNodes: Set<Node> = new Set();
// 各 Hurtbox ノードごとの最終ヒット時刻(hitInterval 用)
private _lastHitTimeMap: Map<Node, number> = new Map();
// 1 フレーム内でヒット済みのターゲット数カウンタ
private _hitCountThisFrame: number = 0;
onLoad() {
// Collider2D を取得
this._collider = this.getComponent(Collider2D);
if (!this._collider) {
console.error('[HitboxComponent] Collider2D がアタッチされていません。Hitbox は動作しません。ノード:', this.node.name);
return;
}
// 攻撃判定用に sensor (= trigger) として扱う
this._collider.sensor = true;
// 接触イベント登録
this._collider.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
if (this.debugLog) {
console.log('[HitboxComponent] onLoad 完了。ノード:', this.node.name);
}
}
onEnable() {
this._hitCountThisFrame = 0;
// フレームごとにカウンタをリセットするためのコールバック登録
director.on(director.EVENT_BEFORE_UPDATE, this._resetFrameHitCount, this);
}
onDisable() {
director.off(director.EVENT_BEFORE_UPDATE, this._resetFrameHitCount, this);
}
onDestroy() {
if (this._collider) {
this._collider.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
}
director.off(director.EVENT_BEFORE_UPDATE, this._resetFrameHitCount, this);
}
/**
* フレームの最初に 1 フレーム内ヒット数カウンタをリセット
*/
private _resetFrameHitCount() {
this._hitCountThisFrame = 0;
}
/**
* Collider2D の接触開始イベント
*/
private _onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
if (!this.enabledHitbox) {
return;
}
if (!this._collider) {
// onLoad でエラー済み
return;
}
const targetNode = otherCollider.node;
if (!targetNode || !targetNode.isValid) {
return;
}
// 1 フレームのヒット数制限チェック
if (this.maxTargetsPerFrame > 0 && this._hitCountThisFrame >= this.maxTargetsPerFrame) {
if (this.debugLog) {
console.log('[HitboxComponent] 1フレーム内の最大ヒット数に達したため、これ以上のダメージ送信は行いません。');
}
return;
}
// ダメージ量チェック
if (this.damageAmount <= 0) {
if (this.debugLog) {
console.warn('[HitboxComponent] damageAmount が 0 以下のため、ダメージは送信されません。ノード:', this.node.name);
}
return;
}
// hitOnce チェック
if (this.hitOnce && this._alreadyHitNodes.has(targetNode)) {
if (this.debugLog) {
console.log('[HitboxComponent] すでにヒット済みのターゲットのためスキップ:', targetNode.name);
}
return;
}
// hitInterval チェック
if (this.hitInterval > 0) {
const now = director.getTotalTime() / 1000; // ms → 秒
const lastHitTime = this._lastHitTimeMap.get(targetNode) ?? -Infinity;
if (now - lastHitTime < this.hitInterval) {
if (this.debugLog) {
console.log('[HitboxComponent] クールタイム中のためスキップ:', targetNode.name);
}
return;
}
this._lastHitTimeMap.set(targetNode, now);
}
// Hurtbox を探してダメージ送信
const damageSent = this._sendDamageToHurtbox(targetNode);
if (damageSent) {
this._hitCountThisFrame++;
if (this.hitOnce) {
this._alreadyHitNodes.add(targetNode);
}
}
}
/**
* 対象ノードのコンポーネントから Hurtbox (receiveDamage メソッドを持つもの) を探してダメージ送信
*/
private _sendDamageToHurtbox(targetNode: Node): boolean {
const comps = targetNode.getComponents(Component);
if (!comps || comps.length === 0) {
if (this.debugLog) {
console.warn('[HitboxComponent] 対象ノードにコンポーネントがありません。Hurtbox が見つかりません。ターゲット:', targetNode.name);
}
return false;
}
// ノックバックベクトル計算(オプション)
let knockbackVec: Vec2 | undefined = undefined;
if (this.knockbackPower > 0) {
const dir = this._calculateDirection2D(this.node, targetNode);
if (dir) {
knockbackVec = new Vec2(dir.x * this.knockbackPower, dir.y * this.knockbackPower);
}
}
const damageInfo: DamageInfo = {
amount: this.damageAmount,
hitboxNode: this.node,
knockback: knockbackVec,
hitType: this.hitType || undefined,
};
let sent = false;
for (const comp of comps) {
// any キャストして、動的に receiveDamage を呼ぶ
const anyComp = comp as any;
if (typeof anyComp.receiveDamage === 'function') {
try {
anyComp.receiveDamage(damageInfo);
sent = true;
if (this.debugLog) {
console.log(
'[HitboxComponent] ダメージ送信成功:',
`ターゲット=${targetNode.name}`,
`コンポーネント=${comp.constructor.name}`,
`ダメージ=${damageInfo.amount}`,
`タイプ=${damageInfo.hitType}`,
`ノックバック=${damageInfo.knockback ? damageInfo.knockback.toString() : 'なし'}`
);
}
} catch (e) {
console.error('[HitboxComponent] receiveDamage 呼び出し中にエラーが発生しました。', e);
}
}
}
if (!sent && this.debugLog) {
console.warn('[HitboxComponent] Hurtbox (receiveDamage を持つコンポーネント) が見つかりませんでした。ターゲット:', targetNode.name);
}
return sent;
}
/**
* 2D 平面上で nodeA → nodeB 方向の正規化ベクトルを計算
*/
private _calculateDirection2D(fromNode: Node, toNode: Node): Vec2 | null {
if (!fromNode.isValid || !toNode.isValid) {
return null;
}
const fromWorldPos = new Vec3();
const toWorldPos = new Vec3();
fromNode.getWorldPosition(fromWorldPos);
toNode.getWorldPosition(toWorldPos);
const dir3 = new Vec3(
toWorldPos.x - fromWorldPos.x,
toWorldPos.y - fromWorldPos.y,
0
);
const length = Math.sqrt(dir3.x * dir3.x + dir3.y * dir3.y);
if (length === 0) {
return null;
}
return new Vec2(dir3.x / length, dir3.y / length);
}
/**
* 外部から Hitbox の状態をリセットしたいとき用のヘルパー
* - hitOnce のヒット履歴
* - hitInterval の最終ヒット時間
* をクリアします。
*/
public resetHitState() {
this._alreadyHitNodes.clear();
this._lastHitTimeMap.clear();
if (this.debugLog) {
console.log('[HitboxComponent] ヒット状態をリセットしました。');
}
}
}
主要部分の解説
-
onLoadCollider2Dを取得し、存在しなければconsole.errorを出しつつ機能停止。collider.sensor = true;でトリガーとして扱うように設定。BEGIN_CONTACTイベントに_onBeginContactを登録。
-
onEnable / onDisable / onDestroydirector.EVENT_BEFORE_UPDATEにフックして、フレーム開始時に_hitCountThisFrameをリセット。- 無効化・破棄時には各種イベント登録を解除してリークを防止。
-
_onBeginContactenabledHitboxが false なら即リターン。- 1 フレーム内ヒット数制限(
maxTargetsPerFrame)をチェック。 - ダメージ量(
damageAmount)が 0 以下なら何もしない。 hitOnceの履歴とhitIntervalのクールタイムをチェック。- 条件を満たしたら
_sendDamageToHurtboxで Hurtbox にダメージ送信。
-
_sendDamageToHurtbox- 対象ノードのすべてのコンポーネントを取得し、
receiveDamageメソッドを持つものを Hurtbox とみなす。 knockbackPowerが > 0 の場合、_calculateDirection2Dで方向を計算し、Vec2のノックバックベクトルを DamageInfo に詰める。- 該当コンポーネントの
receiveDamageをtry-catchで安全に呼び出す。 - 1 つでも送信できれば true を返し、
hitOnceやクールタイム管理側で利用。
- 対象ノードのすべてのコンポーネントを取得し、
-
resetHitState- 外部から「この Hitbox のヒット履歴をクリアしたい」ときに呼び出せるヘルパーメソッド。
- 例: 攻撃アニメーション開始時に呼ぶことで、毎回新しい攻撃として判定させる。
使用手順と動作確認
1. HitboxComponent.ts を作成する
- エディタ下部の Assets パネルで任意のフォルダ(例:
scripts/combat)を右クリックします。 - Create → TypeScript を選択し、ファイル名を
HitboxComponent.tsにします。 - 自動生成されたファイルをダブルクリックして開き、内容をすべて削除して、本記事の
HitboxComponentのコードを貼り付けて保存します。
2. テスト用の Hurtbox コンポーネントを用意する
Hitbox の動作確認用に、簡単な Hurtbox コンポーネントを作っておきます。
これは HitboxComponent に依存しませんが、動作確認のために作るだけです。
- Assets パネルで右クリック → Create → TypeScript を選択し、
TestHurtbox.tsを作成します。 - 以下のような簡易実装を貼り付けます。
import { _decorator, Component, Node } from 'cc';
import type { DamageInfo } from './HitboxComponent';
const { ccclass, property } = _decorator;
@ccclass('TestHurtbox')
export class TestHurtbox extends Component {
@property({
tooltip: 'この Hurtbox の現在 HP。0 以下になるとログを出します。',
})
public hp: number = 100;
receiveDamage(damage: DamageInfo) {
this.hp -= damage.amount;
console.log(
`[TestHurtbox] ダメージを受けました: amount=${damage.amount}, ` +
`残りHP=${this.hp}, type=${damage.hitType}, from=${damage.hitboxNode.name}, ` +
`knockback=${damage.knockback ? damage.knockback.toString() : 'なし'}`
);
if (this.hp <= 0) {
console.log('[TestHurtbox] HP が 0 以下になりました。ノード:', this.node.name);
}
}
}
このコンポーネントは、HitboxComponent から送られてくる DamageInfo を受け取り、HP を減らしつつログを出すだけのシンプルな Hurtbox です。
3. Hurtbox ノード(被弾側)をセットアップ
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、
HurtboxNodeという名前に変更します。 HurtboxNodeを選択し、Inspector の Add Component → Physics 2D → BoxCollider2D を追加します。
– テストのため、サイズやオフセットはデフォルトのままで構いません。- 同じく Inspector で Add Component → Custom → TestHurtbox を追加します。
- 必要に応じて
TestHurtboxのhpを 50 などに設定しておきます。
4. Hitbox ノード(攻撃側)をセットアップ
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、
HitboxNodeという名前に変更します。 HitboxNodeを選択し、Inspector の Add Component → Physics 2D → BoxCollider2D を追加します。
– Is Trigger(またはsensor)のチェックは、スクリプト側でsensor = trueにするので必須ではありませんが、見た目上チェックしておいても構いません。- Inspector の Add Component → Custom → HitboxComponent を追加します。
- HitboxComponent のプロパティを以下のように設定してみます。
- Enabled Hitbox: チェック ON
- Damage Amount: 25
- Hit Type:
melee - Knockback Power: 100
- Hit Once: OFF(連続ダメージを確認するため)
- Hit Interval: 0.5(同じ Hurtbox には 0.5 秒ごとにダメージ)
- Max Targets Per Frame: 0(無制限)
- Debug Log: ON(挙動確認のため)
HitboxNodeとHurtboxNodeが画面上で重なるように、Position を調整します。
5. 2D 物理システムの有効化
Collider2D の接触イベントを受け取るには、2D 物理システムが有効になっている必要があります。
- メニューから Project → Project Settings を開きます。
- 左側のリストから Physics → 2D を選択します。
- Enable(有効化)にチェックが入っていることを確認します。
- 必要であれば重力などを調整しますが、今回のテストでは特に変更不要です。
6. シーンを再生して動作確認
- エディタ右上の Play ボタンを押してシーンを再生します。
- 再生中、
HitboxNodeとHurtboxNodeが重なっていれば、コンソールに以下のようなログが表示されます。[HitboxComponent] ダメージ送信成功: ...[TestHurtbox] ダメージを受けました: amount=25, 残りHP=...
- 0.5 秒ごとに HP が 25 ずつ減っていくことを確認できます。
HitboxComponentの Hit Once を ON にして再生すると、最初の 1 回だけダメージが入り、その後は入らなくなることが確認できます。
7. 攻撃アニメーションとの連携(応用)
例えば、プレイヤーの攻撃アニメーションの開始フレームで以下のように呼び出すことで、毎回新しい攻撃として Hitbox をリセットできます。
// どこかのスクリプトから
const hitbox = this.node.getComponent(HitboxComponent);
if (hitbox) {
hitbox.resetHitState();
hitbox.enabledHitbox = true; // 攻撃開始時に有効化
}
攻撃終了フレームで enabledHitbox = false; にすれば、当たり判定の ON/OFF を簡単に制御できます。
まとめ
本記事では、Cocos Creator 3.8.7 + TypeScript で動作する汎用攻撃判定コンポーネント HitboxComponent を実装しました。
- Collider2D を利用したシンプルな攻撃判定(Hitbox)を、コンポーネント単体で完結するように設計。
- Inspector からダメージ量・ノックバック強さ・クールタイム・1 度きり判定などを柔軟に設定可能。
- Hurtbox 側は
receiveDamage(damage: DamageInfo)を持つだけでよく、特定の GameManager やシングルトンに依存しない。 - 防御的な実装として、Collider2D がない場合のエラーログや、メソッド呼び出し時の
try-catchを用意。
このコンポーネントをベースに、
- ステータスシステム(攻撃力・属性など)と連携した高度なダメージ計算
- 攻撃ヒット時のエフェクト生成(パーティクル・ヒットストップなど)
- 当たり判定の形状やサイズをアニメーションに合わせて制御
といった拡張も容易に行えます。
「攻撃判定」という頻出ロジックを汎用コンポーネントとして切り出すことで、プロジェクト全体の再利用性と保守性が大きく向上します。ぜひ自分のゲームに合わせてカスタマイズしてみてください。




