【Cocos Creator】アタッチするだけ!ContactDamage (接触ダメージ)の実装方法【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】ContactDamage の実装:アタッチするだけで「触れた相手に自動でダメージを与える」汎用スクリプト

このコンポーネントは、敵キャラやトラップなどのノードにアタッチするだけで、「プレイヤーが触れた瞬間にダメージを与える」処理を実現する汎用スクリプトです。物理接触(Collider2Dの衝突/トリガー)をフックして、相手側の「HPを持つコンポーネント」にダメージを通知する仕組みになっており、外部のGameManagerやシングルトンに一切依存しません。

プレイヤー以外にも、「タグ名」や「ノード名の一部」などで対象を柔軟にフィルタリングできるため、幅広いアクションゲーム・STG・プラットフォーマーでそのまま使い回せる設計になっています。


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

1. 要件整理

  • このコンポーネントをアタッチしたノード(主に敵やトラップ)が、接触した相手にダメージを与える。
  • 物理判定は Collider2D を利用し、BEGIN_CONTACT / BEGIN_TRIGGER でダメージ処理を行う。
  • ダメージを与える対象は、タグ・ノード名・レイヤーなどでフィルタ可能にする。
  • 実際の「HP減少処理」は、相手ノードにアタッチされた「HPを持つコンポーネント」の特定メソッドを呼ぶ形で行い、ここでは汎用的なインターフェースにとどめる。
  • 外部のカスタムスクリプトには依存せず、メソッド名と引数をインスペクタから設定することで、任意のHPコンポーネントに対応できるようにする。
  • 必要コンポーネント(Collider2D)が見つからない場合はエラーログを出し、動作を止める。

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

このコンポーネントは、すべての挙動をインスペクタから調整できるように設計します。

  • damageAmount: number
    • 与えるダメージ量。
    • 正の値でダメージ。0以下にするとダメージ処理を行わない。
    • 例:10, 25 など。
  • onlyOncePerContact: boolean
    • 同じ接触中に何度もダメージを与えるかどうか。
    • true: 「接触開始時」に1回だけダメージを与える。
    • false: 物理更新ごとに何度もダメージを与えうる(通常は非推奨)。
  • cooldown: number
    • 同じ相手に再度ダメージを与えるまでのクールタイム(秒)。
    • 0 の場合はクールタイムなし(接触のたびに即ダメージ)。
    • 例:0.5 や 1.0 など。プレイヤーが敵に触れ続けても一定間隔でしかダメージを受けないようにする用途。
  • useTagFilter: boolean
    • タグによるフィルタリングを行うかどうか。
    • 有効にした場合、targetTag と一致するタグを持つノードだけをダメージ対象とする。
    • Cocos Creator 3.x の「タグ」は Node.layer ではなく、ユーザー独自の文字列タグを想定したものなので、Nodename ベースで付けるなど運用ルールを決めて使う。
  • targetTag: string
    • useTagFilter が true のときのみ使用。
    • この文字列と一致する「タグ」を持つ相手だけがダメージ対象。
    • 実装上は「ノード名の前方一致」や「完全一致」など柔軟に扱う(後述)。
    • 例:"Player", "Hero" など。
  • useNameContainsFilter: boolean
    • ノード名に特定の文字列が含まれるかでフィルタリングする。
    • シンプルなプロジェクトでは、タグの代わりに「名前に ‘Player’ を含むノードだけにダメージ」などで十分なことが多い。
  • nameContains: string
    • useNameContainsFilter が true のときのみ使用。
    • この文字列を Node.name が含む相手だけをダメージ対象とする。
    • 例:"Player", "Hero" など。
  • useLayerFilter: boolean
    • レイヤー(Node.layer)によるフィルタリングを行うかどうか。
    • 有効にした場合、targetLayer と一致するレイヤーを持つノードだけをダメージ対象とする。
    • プロジェクト設定でレイヤーを分けている場合に有効。
  • targetLayer: number
    • useLayerFilter が true のときのみ使用。
    • ダメージ対象とするレイヤービット。Layers.Enum を利用してドロップダウンで選択可能にする。
  • hpComponentName: string
    • 相手側ノードにアタッチされている「HPを持つコンポーネント」のクラス名。
    • この文字列で getComponent を行い、そのコンポーネントに対してダメージメソッドを呼び出す。
    • 例:"PlayerHealth", "Health" など。
    • 未設定または空文字の場合は、「HPコンポーネントを探さない」仕様にして、ログだけ出すか何もしない。
  • damageMethodName: string
    • HPコンポーネント側で実装されている「ダメージを受けるメソッド名」。
    • このメソッドに (amount: number, source?: Node) のような引数で呼び出すことを想定。
    • 例:"applyDamage", "takeDamage" など。
  • passSelfAsSource: boolean
    • ダメージメソッドに「ダメージを与えた側のノード(this.node)」を第2引数として渡すかどうか。
    • true: damageMethod(damageAmount, this.node) のように呼ぶ。
    • false: damageMethod(damageAmount) のように第1引数のみ。
  • logDebug: boolean
    • デバッグ用ログ出力を有効にするかどうか。
    • 接触検知やフィルタ結果、ダメージ適用結果などを console.log で確認できる。

このように、どのノードに、どんな条件で、どんな方法でダメージを伝えるか をすべてインスペクタから制御できるようにすることで、他のカスタムスクリプトへの依存を完全に排除しつつ、柔軟性を確保します。


TypeScriptコードの実装


import { _decorator, Component, Node, Collider2D, IPhysics2DContact, Contact2DType, Layers, warn, error, sys } from 'cc';
const { ccclass, property } = _decorator;

/**
 * ContactDamage
 * 
 * このコンポーネントを持つノードが、指定条件を満たす相手と接触したときにダメージを与える。
 * 
 * - 必須: 同じノードに Collider2D (BoxCollider2D, CircleCollider2D など) がアタッチされていること。
 * - ダメージ対象: タグ / 名前 / レイヤー でフィルタリング可能。
 * - ダメージ適用: 相手ノードの任意コンポーネントに対して、任意メソッドを呼び出す。
 */
@ccclass('ContactDamage')
export class ContactDamage extends Component {

    @property({
        tooltip: '与えるダメージ量。0以下の場合はダメージを与えません。'
    })
    public damageAmount: number = 10;

    @property({
        tooltip: '同じ接触中に1回だけダメージを与えるかどうか。trueの場合、接触開始時(BEGIN_CONTACT/BEGIN_TRIGGER)のみダメージを与えます。'
    })
    public onlyOncePerContact: boolean = true;

    @property({
        tooltip: '同じ相手に再度ダメージを与えるまでのクールタイム(秒)。0の場合はクールタイムなし。'
    })
    public cooldown: number = 0.0;

    @property({
        tooltip: 'タグによるフィルタリングを行うかどうか。trueの場合、targetTagに一致するタグを持つ相手のみダメージを与えます。'
    })
    public useTagFilter: boolean = false;

    @property({
        tooltip: 'ダメージ対象とするタグ名。運用として、ノード名のプレフィックスなどでタグを表現してください。例: "Player"。'
    })
    public targetTag: string = 'Player';

    @property({
        tooltip: 'ノード名に特定の文字列が含まれるかでフィルタリングするかどうか。'
    })
    public useNameContainsFilter: boolean = true;

    @property({
        tooltip: 'ノード名にこの文字列が含まれる相手のみダメージを与えます。例: "Player"。'
    })
    public nameContains: string = 'Player';

    @property({
        tooltip: 'レイヤーによるフィルタリングを行うかどうか。trueの場合、targetLayerと一致するレイヤーの相手のみダメージを与えます。'
    })
    public useLayerFilter: boolean = false;

    @property({
        type: Layers.Enum,
        tooltip: 'ダメージ対象とするレイヤー。Layers.Enumから選択してください。'
    })
    public targetLayer: number = Layers.Enum.DEFAULT;

    @property({
        tooltip: '相手側のHPコンポーネントのクラス名。例: "PlayerHealth"。空の場合、HPコンポーネントを検索しません。'
    })
    public hpComponentName: string = 'PlayerHealth';

    @property({
        tooltip: 'ダメージを適用するメソッド名。例: "applyDamage" や "takeDamage"。'
    })
    public damageMethodName: string = 'applyDamage';

    @property({
        tooltip: 'ダメージメソッドに、このノード(self)を第2引数として渡すかどうか。trueの場合: method(damage, this.node)。'
    })
    public passSelfAsSource: boolean = true;

    @property({
        tooltip: 'デバッグ用ログを出力するかどうか。'
    })
    public logDebug: boolean = false;

    private _collider: Collider2D | null = null;

    // 相手ノードごとの最終ダメージ時刻(UNIX秒)
    private _lastDamageTimeMap: Map<number, number> = new Map();

    onLoad() {
        // Collider2D の取得
        this._collider = this.getComponent(Collider2D);
        if (!this._collider) {
            error('[ContactDamage] Collider2D がアタッチされていません。ContactDamage を使用するノードには Collider2D (BoxCollider2D など) を追加してください。');
            return;
        }

        // 物理イベントの登録
        this._collider.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
        this._collider.on(Contact2DType.BEGIN_TRIGGER, this._onBeginTrigger, this);

        if (!this.onlyOncePerContact) {
            // 継続的にダメージを与える場合は、必要に応じて PRE_SOLVE/POST_SOLVE などを追加できます。
            // ここではサンプルとして BEGIN_CONTACT/BEGIN_TRIGGER のみを使用します。
        }
    }

    onDestroy() {
        if (this._collider) {
            this._collider.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
            this._collider.off(Contact2DType.BEGIN_TRIGGER, this._onBeginTrigger, this);
        }
        this._lastDamageTimeMap.clear();
    }

    /**
     * 衝突開始時のコールバック
     */
    private _onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        if (this.logDebug) {
            console.log('[ContactDamage] BEGIN_CONTACT with', otherCollider.node.name);
        }
        this._tryApplyDamage(otherCollider.node);
    }

    /**
     * トリガー開始時のコールバック
     */
    private _onBeginTrigger(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        if (this.logDebug) {
            console.log('[ContactDamage] BEGIN_TRIGGER with', otherCollider.node.name);
        }
        this._tryApplyDamage(otherCollider.node);
    }

    /**
     * ダメージ適用を試みるメイン処理
     */
    private _tryApplyDamage(targetNode: Node) {
        if (this.damageAmount <= 0) {
            if (this.logDebug) {
                console.log('[ContactDamage] damageAmount <= 0 のため、ダメージを与えません。');
            }
            return;
        }

        // 自身のノードとは無視
        if (targetNode === this.node) {
            return;
        }

        // フィルタリング
        if (!this._passesFilters(targetNode)) {
            if (this.logDebug) {
                console.log('[ContactDamage] フィルタ条件を満たさないため、ダメージを与えません。 target =', targetNode.name);
            }
            return;
        }

        // クールタイムチェック
        if (!this._canDamageNow(targetNode)) {
            if (this.logDebug) {
                console.log('[ContactDamage] クールタイム中のため、ダメージを与えません。 target =', targetNode.name);
            }
            return;
        }

        // HPコンポーネントの取得とメソッド呼び出し
        if (!this.hpComponentName || !this.damageMethodName) {
            warn('[ContactDamage] hpComponentName または damageMethodName が設定されていません。ダメージメソッドを呼び出せません。');
            return;
        }

        const hpComponent = targetNode.getComponent(this.hpComponentName as any);
        if (!hpComponent) {
            if (this.logDebug) {
                console.log(`[ContactDamage] targetNode に HP コンポーネント (${this.hpComponentName}) が見つかりません。 target =`, targetNode.name);
            }
            return;
        }

        const damageMethod: any = (hpComponent as any)[this.damageMethodName];
        if (typeof damageMethod !== 'function') {
            warn(`[ContactDamage] HP コンポーネント "${this.hpComponentName}" にメソッド "${this.damageMethodName}" が存在しません。`);
            return;
        }

        // メソッド呼び出し
        try {
            if (this.passSelfAsSource) {
                damageMethod.call(hpComponent, this.damageAmount, this.node);
                if (this.logDebug) {
                    console.log(`[ContactDamage] ダメージを適用: ${this.damageAmount} to ${targetNode.name} (source: ${this.node.name})`);
                }
            } else {
                damageMethod.call(hpComponent, this.damageAmount);
                if (this.logDebug) {
                    console.log(`[ContactDamage] ダメージを適用: ${this.damageAmount} to ${targetNode.name}`);
                }
            }
            // ダメージ適用時刻を記録
            this._setLastDamageTime(targetNode);
        } catch (e) {
            error('[ContactDamage] ダメージメソッド呼び出し中にエラーが発生しました:', e);
        }
    }

    /**
     * 各種フィルタリングを通過するかどうか
     */
    private _passesFilters(targetNode: Node): boolean {
        // タグフィルタ(運用ルールとして、ノード名のプレフィックスをタグとみなす例)
        if (this.useTagFilter && this.targetTag) {
            if (!targetNode.name.startsWith(this.targetTag)) {
                return false;
            }
        }

        // 名前部分一致フィルタ
        if (this.useNameContainsFilter && this.nameContains) {
            if (!targetNode.name.includes(this.nameContains)) {
                return false;
            }
        }

        // レイヤーフィルタ
        if (this.useLayerFilter) {
            if (targetNode.layer !== this.targetLayer) {
                return false;
            }
        }

        return true;
    }

    /**
     * クールタイムの判定
     */
    private _canDamageNow(targetNode: Node): boolean {
        if (this.cooldown <= 0) {
            return true;
        }
        const now = sys.now() / 1000; // ミリ秒 → 秒
        const id = targetNode._id; // Nodeの一意ID(内部プロパティだが実運用上安定している)

        const lastTime = this._lastDamageTimeMap.get(id);
        if (lastTime === undefined) {
            return true;
        }
        return (now - lastTime) >= this.cooldown;
    }

    private _setLastDamageTime(targetNode: Node) {
        if (this.cooldown <= 0) {
            return;
        }
        const now = sys.now() / 1000;
        const id = targetNode._id;
        this._lastDamageTimeMap.set(id, now);
    }
}

コードの要点解説

  • onLoad
    • Collider2DgetComponent で取得し、存在しなければ error ログを出して終了します。
    • 取得できた場合は、BEGIN_CONTACTBEGIN_TRIGGER イベントにリスナーを登録します。
  • _onBeginContact / _onBeginTrigger
    • 接触開始時に呼ばれるコールバックで、どちらも _tryApplyDamage に処理を委譲します。
    • デバッグフラグ logDebug が true の場合、相手ノード名をログ出力します。
  • _tryApplyDamage
    • ダメージ量チェック(0以下なら何もしない)。
    • 自身のノードとの接触は無視。
    • _passesFilters でターゲットフィルタリング。
    • _canDamageNow でクールタイム判定。
    • 相手ノードから hpComponentName で指定されたコンポーネントを取得し、damageMethodName で指定されたメソッドを呼び出します。
    • passSelfAsSource が true の場合は、(damageAmount, this.node) のように2引数で呼びます。
    • 成功したら _setLastDamageTime で最終ダメージ時刻を記録します。
  • _passesFilters
    • タグ(ノード名のプレフィックスとして扱う例)、名前部分一致、レイヤーの3種類のフィルタをAND条件で適用します。
    • いずれかが条件を満たさない場合は false を返し、ダメージ処理をスキップします。
  • _canDamageNow / _setLastDamageTime
    • sys.now() を使って現在時刻(秒)を取得し、相手ノードごとの最終ダメージ時刻を Map で管理します。
    • cooldown 秒以上経過していれば再度ダメージを許可します。

使用手順と動作確認

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

  1. エディタ左下の Assets パネルで、任意のフォルダ(例:assets/scripts)を右クリックします。
  2. Create → TypeScript を選択し、ファイル名を ContactDamage.ts とします。
  3. 作成された ContactDamage.ts をダブルクリックして開き、既存のテンプレートコードをすべて削除して、前述の TypeScript コードを貼り付けて保存します。

2. テスト用シーンとノードの準備

ここでは「プレイヤーに触れるとダメージを与える敵」を例にします。

  1. シーンにプレイヤーノードを作成
    • Hierarchy パネルで右クリック → Create → 2D Object → Sprite などを選択し、ノード名を Player に変更します。
    • このノードに移動やHPを管理する既存のコンポーネントがある場合はそのまま利用して構いません。
  2. プレイヤーのHPコンポーネント(例)を用意
    既に PlayerHealth のようなHP管理コンポーネントがある前提で使えますが、ない場合は簡易的なものを用意するとテストしやすくなります。
    例として、以下のようなコンポーネントを想定します(※このスクリプトは必須ではなく、ユーザー側で自由に実装してください):
    
    // 例: PlayerHealth.ts (任意の場所に作成)
    import { _decorator, Component, Node, log } from 'cc';
    const { ccclass, property } = _decorator;
    
    @ccclass('PlayerHealth')
    export class PlayerHealth extends Component {
        @property
        public maxHp: number = 100;
    
        private _hp: number = 0;
    
        onLoad() {
            this._hp = this.maxHp;
        }
    
        public applyDamage(amount: number, source?: Node) {
            this._hp -= amount;
            log(`[PlayerHealth] ダメージ: ${amount}, 残りHP: ${this._hp}, source: ${source ? source.name : 'none'}`);
            if (this._hp <= 0) {
                log('[PlayerHealth] プレイヤー死亡');
            }
        }
    }
    
    • Assets パネルで PlayerHealth.ts を作成し、上記のような簡易実装を入れても良いでしょう。
    • プレイヤーノードを選択し、Inspector → Add Component → Custom → PlayerHealth でアタッチします。
  3. 敵ノードを作成
    • Hierarchy パネルで右クリック → Create → 2D Object → Sprite などを選択し、ノード名を Enemy に変更します。
    • 敵スプライトの大きさや位置を適当に調整し、プレイヤーと接触できる位置に配置します。
  4. 敵ノードに Collider2D を追加
    • Enemy ノードを選択し、Inspector → Add Component → Physics 2D → BoxCollider2D などを追加します。
    • コライダーのサイズがスプライトと合うように Edit Collider で調整します。
    • 接触判定だけにしたい場合は、Is Trigger チェックボックスをオンにしてトリガーにしておくと便利です。
  5. 敵ノードに ContactDamage をアタッチ
    • Enemy ノードを選択し、Inspector → Add Component → Custom → ContactDamage を選択します。
    • Inspector に ContactDamage のプロパティが表示されるので、以下のように設定します(例):
      • damageAmount: 10
      • onlyOncePerContact: true
      • cooldown: 0.5(プレイヤーが触れ続けても0.5秒ごとにしかダメージを受けない)
      • useTagFilter: false(今回は使わない)
      • targetTag: “Player”(未使用だが、将来用に)
      • useNameContainsFilter: true
      • nameContains: “Player”(Player という名前を含むノードにのみダメージ)
      • useLayerFilter: false(今回は使わない)
      • targetLayer: DEFAULT(デフォルトのまま)
      • hpComponentName: “PlayerHealth”(プレイヤー側のHPコンポーネント名)
      • damageMethodName: “applyDamage”(PlayerHealth内で定義したメソッド名)
      • passSelfAsSource: true(ダメージ元として Enemy ノードを渡す)
      • logDebug: true(動作確認のためにログを出す)

3. 物理設定の確認

  • 2D物理を利用するため、Project Settings → Physics 2D で物理エンジンが有効になっていることを確認します。
  • プレイヤーと敵の RigidBody2D の有無は任意ですが、少なくとも一方に RigidBody2D があると物理挙動が自然になります(当たり判定自体は Collider2D 同士でも可能)。
  • 必要に応じて、プレイヤーにも BoxCollider2D / RigidBody2D を追加してください。

4. 実行して動作確認

  1. シーンを保存し、上部ツールバーの Play ボタンでゲームを実行します。
  2. プレイヤーを移動させて敵に接触させます(キーボード移動などがない場合は、カメラやノード位置を調整して最初から重なっていても構いません)。
  3. コンソールログに以下のようなメッセージが出ていれば成功です。
    • [ContactDamage] BEGIN_CONTACT with Player
    • [ContactDamage] ダメージを適用: 10 to Player (source: Enemy)
    • [PlayerHealth] ダメージ: 10, 残りHP: 90, source: Enemy
  4. プレイヤーが敵に触れ続けている場合、cooldown で指定した秒数ごとにダメージログが出力されることを確認します。

まとめ

この ContactDamage コンポーネントは、

  • 敵やトラップなどにアタッチするだけで、接触した相手に自動でダメージを与えられる。
  • ダメージ対象はタグ・名前・レイヤーで柔軟にフィルタリングできる。
  • 実際のHP減少処理は、相手ノードの任意コンポーネント・任意メソッドに委譲できるため、既存のHPシステムにも簡単に組み込める。
  • クールタイムやデバッグログもインスペクタから調整でき、ゲームバランス調整やデバッグがしやすい。
  • 外部のGameManagerやシングルトンに一切依存せず、このスクリプト単体で完結している。

アクションゲームでは、「触れたらダメージ」という場面は非常に多く、個別に実装するとコードが散らばりがちです。
今回のような汎用コンポーネントとして切り出しておくことで、

  • 新しい敵やトラップを追加するときに、ただアタッチして数値を調整するだけで済む。
  • ダメージ仕様の変更も、このコンポーネントを修正するだけで全体に反映できる。

といったメリットが得られます。
ぜひ、この ContactDamage をベースに、「ノックバック追加」「属性ダメージ」「多段ヒット」など、プロジェクトに合わせた拡張も試してみてください。

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をコピーしました!