【Cocos Creator】アタッチするだけ!DamageZone (継続ダメージ床)の実装方法【TypeScript】

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

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

脱・初心者!Godot 4 ゲーム開発の「2歩目」

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

Godot4ローグライク入門 ~ダンジョン自動生成~

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

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

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

【Cocos Creator 3.8】DamageZone(継続ダメージ床)の実装:アタッチするだけで「入っている間だけ毎秒ダメージイベントを発行するエリア」を実現する汎用スクリプト

このコンポーネントは、毒の沼地や溶岩、トゲ床など「乗っている(接触している)間、一定間隔でダメージを与えるゾーン」を簡単に作るための汎用スクリプトです。
任意のノードに DamageZone をアタッチし、コライダーとプロパティを設定するだけで、接触している相手に対して「イベントベース」でダメージ情報を通知できます。

ダメージの処理(HPを減らすなど)は、このコンポーネントから発行されるイベントを受け取る側(プレイヤーや敵キャラ)のスクリプトで実装します。
これにより、DamageZone 自体はどんなゲームでも再利用可能な、完全に独立したコンポーネントとして設計できます。


コンポーネントの設計方針

実現したい機能

  • このコンポーネントをアタッチしたノードを「ダメージゾーン」とする。
  • 他のノードがこのゾーンに入っている間、一定間隔でダメージイベントを発行する。
  • イベントには少なくとも以下の情報を含める:
    • ダメージ量(数値)
    • ダメージタイプ(文字列)
    • ダメージを受ける対象ノード(Node
    • ダメージを発生させたゾーン側のノード(Node
  • 「ゾーンに入った」「ゾーンから出た」タイミングでもイベントを発行できるようにする。
  • 物理エンジンは 2D / 3D どちらでも使えるようにしつつ、依存は「コライダーコンポーネント」に留める。
  • 外部の GameManager やシングルトンに一切依存しない。

外部依存をなくすためのアプローチ

  • ダメージ処理(HP管理など)は一切行わず、「イベントを発行するだけ」に徹する。
  • イベントの送信先は「接触している対象ノード」に限定し、Node.emit / Node.dispatchEvent を使う。
  • イベント名やダメージ量、ダメージタイプなどはすべてインスペクタから設定可能にする。
  • 2D / 3D どちらのコライダーでも使えるように、Collider2DCollider(3D)の両方をサポートする。
  • コライダーが見つからない場合はエラーログを出し、エディタでの追加手順を記事内で明示する。

インスペクタで設定可能なプロパティ

以下のプロパティを @property で公開し、インスペクタから自由に調整できるようにします。

  • use2DPhysics (boolean)
    • 2D 物理(Collider2D)を使うか、3D 物理(Collider)を使うかの切り替え。
    • true:2D コライダーを使用(BoxCollider2D など)。
    • false:3D コライダーを使用(BoxCollider など)。
  • damageAmount (number)
    • 1 回あたりのダメージ量。
    • 例:10, 5, 1 など。
  • damageInterval (number)
    • ダメージを与える間隔(秒)。
    • 例:1.0 で「毎秒 1 回」、0.5 で「毎秒 2 回」など。
    • 0 以下にすると毎フレームダメージになってしまうため、コード側で下限(例:0.01 秒)を設ける。
  • damageType (string)
    • ダメージの種類を識別する文字列。
    • 例:"poison", "lava", "spike" など。
    • 受け手側がダメージの種類ごとに処理を変えたい場合に使用。
  • sendEnterEvent (boolean)
    • ゾーンに入った瞬間に「ダメージゾーンに入った」イベントを送るかどうか。
  • sendExitEvent (boolean)
    • ゾーンから出た瞬間に「ダメージゾーンから出た」イベントを送るかどうか。
  • damageEventName (string)
    • 継続ダメージ時に送るイベント名。
    • デフォルト:"damage-zone-damage"
  • enterEventName (string)
    • ゾーンに入ったときに送るイベント名。
    • デフォルト:"damage-zone-enter"
  • exitEventName (string)
    • ゾーンから出たときに送るイベント名。
    • デフォルト:"damage-zone-exit"
  • debugLog (boolean)
    • イベント発行時やエラー時に console.log を出すかどうか。
    • 開発中は true にして挙動確認、本番では false にする想定。

イベントのペイロード(受け手側が受け取るオブジェクト)は、以下のような構造にします:

interface DamageZoneEventPayload {
    amount: number;        // ダメージ量
    type: string;          // ダメージタイプ
    sourceZone: Node;      // ダメージゾーン側のノード
    target: Node;          // ダメージを受ける対象ノード(イベント送信先のノードと同じ)
    interval: number;      // ダメージ間隔(秒)
}

※ このインターフェースは型の説明用で、実際のコンポーネント内では明示的なエクスポートは行わず、オブジェクトリテラルとして生成します。


TypeScriptコードの実装

以下が完成した DamageZone.ts の全コードです。


import { _decorator, Component, Node, Collider2D, Contact2DType, IPhysics2DContact, Collider, ITriggerEvent, director } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('DamageZone')
export class DamageZone extends Component {

    @property({
        tooltip: '2D物理(Collier2D)を使用する場合はON。3D物理(Collider)を使用する場合はOFF。'
    })
    public use2DPhysics: boolean = true;

    @property({
        tooltip: '1回あたりのダメージ量。'
    })
    public damageAmount: number = 10;

    @property({
        tooltip: 'ダメージを与える間隔(秒)。0.1なら1秒間に10回ダメージ。'
    })
    public damageInterval: number = 1.0;

    @property({
        tooltip: 'ダメージの種類を表す文字列。例: "poison", "lava", "spike" など。'
    })
    public damageType: string = 'poison';

    @property({
        tooltip: 'ゾーンに入った瞬間にイベントを送るかどうか。'
    })
    public sendEnterEvent: boolean = true;

    @property({
        tooltip: 'ゾーンから出た瞬間にイベントを送るかどうか。'
    })
    public sendExitEvent: boolean = true;

    @property({
        tooltip: '継続ダメージ時に送信するイベント名。対象ノードに対して emit されます。'
    })
    public damageEventName: string = 'damage-zone-damage';

    @property({
        tooltip: 'ゾーンに入ったときに送信するイベント名。対象ノードに対して emit されます。'
    })
    public enterEventName: string = 'damage-zone-enter';

    @property({
        tooltip: 'ゾーンから出たときに送信するイベント名。対象ノードに対して emit されます。'
    })
    public exitEventName: string = 'damage-zone-exit';

    @property({
        tooltip: 'デバッグログを有効にするかどうか。開発中の動作確認用。'
    })
    public debugLog: boolean = false;

    // ==== 内部状態 ====

    /**
     * 現在このゾーンに接触している対象ノードのセット。
     */
    private _targets: Set<Node> = new Set();

    /**
     * 前回ダメージを与えた時刻(秒)。director.getTotalTime() を使用。
     */
    private _lastDamageTime: number = 0;

    /**
     * 使用中の2Dコライダーへの参照(use2DPhysics=true の場合に使用)。
     */
    private _collider2D: Collider2D | null = null;

    /**
     * 使用中の3Dコライダーへの参照(use2DPhysics=false の場合に使用)。
     */
    private _collider3D: Collider | null = null;

    onLoad() {
        // ダメージ間隔の下限チェック
        if (this.damageInterval <= 0) {
            if (this.debugLog) {
                console.warn(`[DamageZone] damageInterval が 0 以下です (${this.damageInterval})。0.1 に補正します。`);
            }
            this.damageInterval = 0.1;
        }

        // コライダーの取得とイベント登録
        if (this.use2DPhysics) {
            this._setup2DCollider();
        } else {
            this._setup3DCollider();
        }
    }

    onDisable() {
        // 無効化されたときはターゲットをクリア
        this._targets.clear();
        this._unregister2DEvents();
        this._unregister3DEvents();
    }

    onDestroy() {
        this._targets.clear();
        this._unregister2DEvents();
        this._unregister3DEvents();
    }

    update(deltaTime: number) {
        if (this._targets.size === 0) {
            return;
        }

        const now = director.getTotalTime() / 1000; // ms → 秒
        if (now - this._lastDamageTime < this.damageInterval) {
            return;
        }

        this._lastDamageTime = now;

        // 接触中のすべての対象にダメージイベントを送信
        this._applyDamageToTargets();
    }

    // ==== 2Dコライダーのセットアップ ====

    private _setup2DCollider() {
        this._collider2D = this.getComponent(Collider2D);
        if (!this._collider2D) {
            console.error('[DamageZone] Collider2D が見つかりません。use2DPhysics=true の場合は、BoxCollider2D などの 2D コライダーをこのノードに追加してください。');
            return;
        }

        // Trigger(isTrigger=true)であることを推奨
        if (!this._collider2D.sensor) {
            if (this.debugLog) {
                console.warn('[DamageZone] 2Dコライダーが sensor=false です。DamageZone として使う場合は sensor=true (トリガー) を推奨します。');
            }
        }

        this._collider2D.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact2D, this);
        this._collider2D.on(Contact2DType.END_CONTACT, this._onEndContact2D, this);
    }

    private _unregister2DEvents() {
        if (this._collider2D) {
            this._collider2D.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact2D, this);
            this._collider2D.off(Contact2DType.END_CONTACT, this._onEndContact2D, this);
        }
    }

    private _onBeginContact2D(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        const targetNode = otherCollider.node;
        this._onTargetEnter(targetNode);
    }

    private _onEndContact2D(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        const targetNode = otherCollider.node;
        this._onTargetExit(targetNode);
    }

    // ==== 3Dコライダーのセットアップ ====

    private _setup3DCollider() {
        this._collider3D = this.getComponent(Collider);
        if (!this._collider3D) {
            console.error('[DamageZone] Collider (3D) が見つかりません。use2DPhysics=false の場合は、BoxCollider などの 3D コライダーをこのノードに追加してください。');
            return;
        }

        if (!this._collider3D.isTrigger) {
            if (this.debugLog) {
                console.warn('[DamageZone] 3Dコライダーが isTrigger=false です。DamageZone として使う場合は isTrigger=true (トリガー) を推奨します。');
            }
        }

        this._collider3D.on('onTriggerEnter', this._onTriggerEnter3D, this);
        this._collider3D.on('onTriggerExit', this._onTriggerExit3D, this);
    }

    private _unregister3DEvents() {
        if (this._collider3D) {
            this._collider3D.off('onTriggerEnter', this._onTriggerEnter3D, this);
            this._collider3D.off('onTriggerExit', this._onTriggerExit3D, this);
        }
    }

    private _onTriggerEnter3D(event: ITriggerEvent) {
        const otherCollider = event.otherCollider;
        const targetNode = otherCollider.node;
        this._onTargetEnter(targetNode);
    }

    private _onTriggerExit3D(event: ITriggerEvent) {
        const otherCollider = event.otherCollider;
        const targetNode = otherCollider.node;
        this._onTargetExit(targetNode);
    }

    // ==== ゾーンへの出入り共通処理 ====

    private _onTargetEnter(target: Node) {
        if (!target || !target.isValid) {
            return;
        }

        if (!this._targets.has(target)) {
            this._targets.add(target);

            if (this.debugLog) {
                console.log(`[DamageZone] Target entered: ${target.name}`);
            }

            if (this.sendEnterEvent && this.enterEventName) {
                const payload = this._createPayload(target);
                target.emit(this.enterEventName, payload);
            }
        }
    }

    private _onTargetExit(target: Node) {
        if (!target || !target.isValid) {
            return;
        }

        if (this._targets.has(target)) {
            this._targets.delete(target);

            if (this.debugLog) {
                console.log(`[DamageZone] Target exited: ${target.name}`);
            }

            if (this.sendExitEvent && this.exitEventName) {
                const payload = this._createPayload(target);
                target.emit(this.exitEventName, payload);
            }
        }
    }

    // ==== ダメージ適用処理 ====

    private _applyDamageToTargets() {
        if (!this.damageEventName) {
            if (this.debugLog) {
                console.warn('[DamageZone] damageEventName が空です。ダメージイベントを送信できません。');
            }
            return;
        }

        this._targets.forEach((target) => {
            if (!target || !target.isValid) {
                return;
            }

            const payload = this._createPayload(target);

            if (this.debugLog) {
                console.log(`[DamageZone] Damage applied to ${target.name}: amount=${payload.amount}, type=${payload.type}`);
            }

            target.emit(this.damageEventName, payload);
        });
    }

    private _createPayload(target: Node) {
        return {
            amount: this.damageAmount,
            type: this.damageType,
            sourceZone: this.node,
            target: target,
            interval: this.damageInterval,
        };
    }
}

コードのポイント解説

onLoad

  • damageInterval が 0 以下の場合、意図せず毎フレームダメージにならないよう、0.1 秒に補正しています。
  • use2DPhysics の値に応じて 2D / 3D コライダーのセットアップ関数を呼び出します。

_setup2DCollider / _setup3DCollider

  • それぞれ Collider2D / CollidergetComponent で取得し、見つからない場合は console.error を出します。
  • トリガー(sensor / isTrigger)であることを推奨し、そうでない場合は警告ログを出します。
  • 2D の場合は Contact2DType.BEGIN_CONTACT / END_CONTACT、3D の場合は onTriggerEnter / onTriggerExit イベントにハンドラを登録しています。

_onTargetEnter / _onTargetExit

  • 接触している対象ノードを Set<Node> で管理し、重複登録を防ぎます。
  • 入った・出たタイミングで、sendEnterEvent / sendExitEvent が有効なら、対象ノードに対してイベントを emit します。
  • イベントのペイロードは _createPayload で毎回生成しています。

update

  • 接触中の対象が 1 つもいなければ何もしません。
  • director.getTotalTime() を使って経過時間を管理し、damageInterval ごとに一度だけダメージを発生させます。
  • ダメージ発生タイミングになったら _applyDamageToTargets を呼び出します。

_applyDamageToTargets

  • 現在接触中のすべての対象ノードに対して、damageEventName でイベントを emit します。
  • イベント名が空文字の場合は警告ログを出し、何も送信しません。

使用手順と動作確認

1. スクリプトファイルの作成

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. 新規スクリプトに DamageZone.ts という名前を付けます。
  3. 作成された DamageZone.ts をダブルクリックし、エディタ(VSCode など)で開きます。
  4. 中身をすべて削除し、本記事の「TypeScriptコードの実装」で示したコードをそのまま貼り付けて保存します。

2. テスト用シーンの準備(2D の例)

ここでは 2D ゲーム(毒の沼地)を想定した例で説明します。

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、PoisonArea という名前のノードを作成します。
  2. PoisonArea を選択し、Inspector の Sprite コンポーネントで、毒沼っぽいテクスチャを設定します。
  3. 同じく Inspector で Add Component → Physics2D → BoxCollider2D を追加します。
    • Size を Sprite のサイズに合わせるか、適宜調整してください。
    • Sensor にチェックを入れて、トリガー(当たり判定のみ)にします。
  4. Inspector の Add Component → Custom → DamageZone を選択してアタッチします。

3. DamageZone のプロパティ設定(2D の例)

PoisonArea ノードを選択し、Inspector で DamageZone コンポーネントの各プロパティを設定します。

  • use2DPhysicstrue(2D なので ON)
  • damageAmount5(1 回あたり 5 ダメージ)
  • damageInterval1.0(毎秒 1 回ダメージ)
  • damageType"poison"
  • sendEnterEventtrue
  • sendExitEventtrue
  • damageEventName"damage-zone-damage"(デフォルトのままでOK)
  • enterEventName"damage-zone-enter"
  • exitEventName"damage-zone-exit"
  • debugLog:動作確認中は true にしておくとログが出て分かりやすいです。

4. ダメージを受ける側の簡易テストスクリプト

DamageZone はイベントを発行するだけなので、受ける側でイベントをハンドルする必要があります。
ここではテスト用に、プレイヤーノードに簡単なスクリプトを付けてログを出してみます。

  1. Assets パネルで右クリック → Create → TypeScriptDamageReceiverTest.ts を作成します。
  2. 以下のコードを貼り付けて保存します。

import { _decorator, Component, Node } from 'cc';
const { ccclass } = _decorator;

@ccclass('DamageReceiverTest')
export class DamageReceiverTest extends Component {

    private _hp: number = 100;

    onLoad() {
        // DamageZone からのイベントを購読
        this.node.on('damage-zone-enter', this._onZoneEnter, this);
        this.node.on('damage-zone-exit', this._onZoneExit, this);
        this.node.on('damage-zone-damage', this._onZoneDamage, this);
    }

    onDestroy() {
        this.node.off('damage-zone-enter', this._onZoneEnter, this);
        this.node.off('damage-zone-exit', this._onZoneExit, this);
        this.node.off('damage-zone-damage', this._onZoneDamage, this);
    }

    private _onZoneEnter(payload: any) {
        console.log(`[DamageReceiverTest] Enter zone: type=${payload.type}, from=${payload.sourceZone.name}`);
    }

    private _onZoneExit(payload: any) {
        console.log(`[DamageReceiverTest] Exit zone: type=${payload.type}, from=${payload.sourceZone.name}`);
    }

    private _onZoneDamage(payload: any) {
        this._hp -= payload.amount;
        console.log(`[DamageReceiverTest] Damage received: amount=${payload.amount}, type=${payload.type}, hp=${this._hp}`);
    }
}

次に、プレイヤーノードを作ってこのスクリプトをアタッチします。

  1. Hierarchy で右クリック → Create → 2D Object → Sprite を選び、Player ノードを作成します。
  2. PlayerRigidBody2DBoxCollider2D を追加します。
    • DamageZone と接触するために、Collider2D 同士が重なるように位置を調整してください。
  3. Player の Inspector → Add Component → Custom → DamageReceiverTest を選択してアタッチします。

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

  1. シーンを保存します(Ctrl+S / Cmd+S)。
  2. エディタ上部の Play(▶) ボタンを押してゲームを再生します。
  3. Player を PoisonArea に重ねる(または物理で落とす)と、コンソールに以下のようなログが出るはずです:
    • [DamageReceiverTest] Enter zone: type=poison, from=PoisonArea
    • [DamageReceiverTest] Damage received: amount=5, type=poison, hp=95
    • [DamageReceiverTest] Damage received: amount=5, type=poison, hp=90
    • …(1秒ごとに減っていく)
    • Player が PoisonArea から離れると:
    • [DamageReceiverTest] Exit zone: type=poison, from=PoisonArea

6. 3D ゲームで使う場合の注意点

3D ゲームで使用する場合は次のようにします。

  1. DamageZone をアタッチするノードに BoxCollider(3D) などの 3D コライダーを追加し、IsTrigger にチェックを入れます。
  2. use2DPhysicsfalse に設定します。
  3. ダメージを受ける側のノードにも 3D Collider(必要に応じて RigidBody など)を設定し、同様にイベントを購読するスクリプトをアタッチします。

まとめ

  • DamageZone は「ゾーンに入っている間、一定間隔でダメージイベントを発行する」ための、完全に独立した汎用コンポーネントです。
  • ダメージ処理そのものは行わず、イベントベースで情報を通知することで、どんなゲームの HP システムにも柔軟に組み込めます。
  • 2D / 3D の両方に対応しており、use2DPhysics の切り替えだけで再利用できます。
  • ダメージ量や間隔、ダメージタイプ、イベント名などはすべてインスペクタから調整できるため、「毒の沼」「溶岩」「トゲ床」「電撃フィールド」など、さまざまなギミックを同じコンポーネントで表現できます。
  • 外部の GameManager やシングルトンに依存しない構成なので、プロジェクト間のコピペやアセット化も容易です。

このコンポーネントをベースに、ノックバック情報を追加したり、一定時間ごとにエフェクトを再生したりといった拡張も簡単に行えます。
まずは本記事のコードをそのまま組み込み、イベントの受け側を自分のゲームの HP システムに合わせて実装してみてください。

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

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

脱・初心者!Godot 4 ゲーム開発の「2歩目」

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

Godot4ローグライク入門 ~ダンジョン自動生成~

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

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

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

URLをコピーしました!