【Cocos Creator】アタッチするだけ!HitboxComponent (攻撃判定)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【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 は、同じノードに以下のコンポーネントが存在することを前提にします。

  • Collider2DBoxCollider2D / CircleCollider2D / PolygonCollider2D など)

しかし、付いていない場合でもエラーで落ちないようにonLoad 内で getComponent(Collider2D) を行い、見つからなければ console.error を出して自動的に機能を無効化します。

また、Collider2D の トリガー(sensor)モード を前提にするため、onLoadcollider.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] ヒット状態をリセットしました。');
        }
    }
}

主要部分の解説

  • onLoad
    • Collider2D を取得し、存在しなければ console.error を出しつつ機能停止。
    • collider.sensor = true; でトリガーとして扱うように設定。
    • BEGIN_CONTACT イベントに _onBeginContact を登録。
  • onEnable / onDisable / onDestroy
    • director.EVENT_BEFORE_UPDATE にフックして、フレーム開始時に _hitCountThisFrame をリセット。
    • 無効化・破棄時には各種イベント登録を解除してリークを防止。
  • _onBeginContact
    • enabledHitbox が false なら即リターン。
    • 1 フレーム内ヒット数制限(maxTargetsPerFrame)をチェック。
    • ダメージ量(damageAmount)が 0 以下なら何もしない。
    • hitOnce の履歴と hitInterval のクールタイムをチェック。
    • 条件を満たしたら _sendDamageToHurtbox で Hurtbox にダメージ送信。
  • _sendDamageToHurtbox
    • 対象ノードのすべてのコンポーネントを取得し、receiveDamage メソッドを持つものを Hurtbox とみなす。
    • knockbackPower が > 0 の場合、_calculateDirection2D で方向を計算し、Vec2 のノックバックベクトルを DamageInfo に詰める。
    • 該当コンポーネントの receiveDamagetry-catch で安全に呼び出す。
    • 1 つでも送信できれば true を返し、hitOnce やクールタイム管理側で利用。
  • resetHitState
    • 外部から「この Hitbox のヒット履歴をクリアしたい」ときに呼び出せるヘルパーメソッド。
    • 例: 攻撃アニメーション開始時に呼ぶことで、毎回新しい攻撃として判定させる。

使用手順と動作確認

1. HitboxComponent.ts を作成する

  1. エディタ下部の Assets パネルで任意のフォルダ(例: scripts/combat)を右クリックします。
  2. Create → TypeScript を選択し、ファイル名を HitboxComponent.ts にします。
  3. 自動生成されたファイルをダブルクリックして開き、内容をすべて削除して、本記事の HitboxComponent のコードを貼り付けて保存します。

2. テスト用の Hurtbox コンポーネントを用意する

Hitbox の動作確認用に、簡単な Hurtbox コンポーネントを作っておきます。
これは HitboxComponent に依存しませんが、動作確認のために作るだけです。

  1. Assets パネルで右クリック → Create → TypeScript を選択し、TestHurtbox.ts を作成します。
  2. 以下のような簡易実装を貼り付けます。

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 ノード(被弾側)をセットアップ

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、HurtboxNode という名前に変更します。
  2. HurtboxNode を選択し、Inspector の Add Component → Physics 2D → BoxCollider2D を追加します。
    – テストのため、サイズやオフセットはデフォルトのままで構いません。
  3. 同じく Inspector で Add Component → Custom → TestHurtbox を追加します。
  4. 必要に応じて TestHurtboxhp を 50 などに設定しておきます。

4. Hitbox ノード(攻撃側)をセットアップ

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、HitboxNode という名前に変更します。
  2. HitboxNode を選択し、Inspector の Add Component → Physics 2D → BoxCollider2D を追加します。
    Is Trigger(または sensor)のチェックは、スクリプト側で sensor = true にするので必須ではありませんが、見た目上チェックしておいても構いません。
  3. Inspector の Add Component → Custom → HitboxComponent を追加します。
  4. 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(挙動確認のため)
  5. HitboxNodeHurtboxNode が画面上で重なるように、Position を調整します。

5. 2D 物理システムの有効化

Collider2D の接触イベントを受け取るには、2D 物理システムが有効になっている必要があります。

  1. メニューから Project → Project Settings を開きます。
  2. 左側のリストから Physics2D を選択します。
  3. Enable(有効化)にチェックが入っていることを確認します。
  4. 必要であれば重力などを調整しますが、今回のテストでは特に変更不要です。

6. シーンを再生して動作確認

  1. エディタ右上の Play ボタンを押してシーンを再生します。
  2. 再生中、HitboxNodeHurtboxNode が重なっていれば、コンソールに以下のようなログが表示されます。
    • [HitboxComponent] ダメージ送信成功: ...
    • [TestHurtbox] ダメージを受けました: amount=25, 残りHP=...
  3. 0.5 秒ごとに HP が 25 ずつ減っていくことを確認できます。
  4. HitboxComponentHit 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 を用意。

このコンポーネントをベースに、

  • ステータスシステム(攻撃力・属性など)と連携した高度なダメージ計算
  • 攻撃ヒット時のエフェクト生成(パーティクル・ヒットストップなど)
  • 当たり判定の形状やサイズをアニメーションに合わせて制御

といった拡張も容易に行えます。
「攻撃判定」という頻出ロジックを汎用コンポーネントとして切り出すことで、プロジェクト全体の再利用性と保守性が大きく向上します。ぜひ自分のゲームに合わせてカスタマイズしてみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!