【Cocos Creator】アタッチするだけ!GroupSignaler (グループ通知)の実装方法【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】GroupSignaler の実装:アタッチするだけで「同じグループのノードに一括通知」を実現する汎用スクリプト

本記事では、特定のイベント(例:キー入力・ボタン押下・トリガー侵入など)をきっかけに、同じ「グループ名」を持つノードへ一括でメッセージ(メソッド呼び出し)を送る汎用コンポーネント GroupSignaler を実装します。

敵グループ全員に「警戒開始」「退避開始」などの通知を送りたいとき、各ノードに同じコンポーネントをアタッチし、インスペクタでグループ名とメソッド名を指定するだけで動作します。外部の GameManager やシングルトンには一切依存しません。


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

機能要件の整理

  • 同じ「グループ名」を共有するノード同士で、簡易的なメッセージ通信(メソッド呼び出し)を行いたい。
  • 通知のトリガーは、以下のような複数パターンから選べるようにする:
    • 手動(他スクリプトから sendSignal() を呼ぶ)
    • シーン開始時(onLoad / start
    • キー入力(指定キーが押されたら)
    • ボタン(Button コンポーネントのクリックイベントから呼ぶ)
    • 2Dコリジョンのトリガー侵入時(Collider2DonTriggerEnter 相当)
  • 通知先は「同じグループ名の GroupSignaler を持つノード」全て。
  • 通知内容は「指定メソッド名を呼び出す」。メソッドは任意のコンポーネントに実装されていればよい。
  • 外部のカスタムスクリプト・シングルトンに依存しない。すべてインスペクタ設定で完結させる。

グループ共有の仕組み

GroupSignaler 自体が「そのノードはどのグループに属するか」を保持し、同クラス内の静的マップでグループごとの参加者を管理します。

  • static _groups: Map<string, Set<GroupSignaler>> のような構造で、groupName ごとに全インスタンスを保持。
  • onLoad / onDestroy で自動的に登録・解除するため、ユーザーが特別な管理コードを書く必要はありません。

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

以下のようなプロパティを用意します。

  • groupName: string
    • 同じグループとして扱うノードに同じ文字列を設定します。例:"Enemy""Ally" など。
    • 空文字の場合はグループに参加せず、警告ログを出します。
  • autoRegister: boolean
    • true の場合、onLoad 時に自動でグループに登録。
    • 通常は true のままで問題ありません。
  • signalMethodName: string
    • 通知時に各ノードで呼び出したいメソッド名。
    • 例:"onGroupAlert""onGroupRetreat" など。
    • 通知先ノードのどのコンポーネントに実装してあっても構いません。
    • メソッドシグネチャは onGroupAlert(sender: Node, payload?: any) のようにしておくと扱いやすいです(引数は任意)。
  • includeSelf: boolean
    • true の場合、自分自身にも通知を送ります。
    • false の場合、自身を除外して他のグループメンバーにのみ通知します。
  • sendOnStart: boolean
    • true の場合、start() のタイミングで自動的に sendSignal() を呼びます。
  • triggerType: enum
    • 通知を発火させるトリガー種別。
    • 例:
      • None …… 自動では送信せず、他コンポーネントやボタンから sendSignal() を呼ぶ。
      • KeyDown …… 指定キーが押されたら送信。
      • TriggerEnter2D …… 2Dコライダーのトリガー侵入で送信。
  • keyCode: KeyCode
    • triggerType = KeyDown の時に使用するキー。
    • 例:KeyCode.KEY_E なら「Eキー」で通知します。
  • requireTagOnCollider: string
    • triggerType = TriggerEnter2D の時に、侵入してきた相手に特定のタグ(Node.namegroup など)を要求したい場合に使用。
    • 空文字なら無条件でトリガーします。
    • 簡易実装として、ここでは 相手ノードの name と比較 することにします。
  • payloadJson: string
    • 通知時に一緒に送りたいデータを JSON 文字列で指定。
    • 例:{"alertLevel":2,"reason":"player_spotted"}
    • 送信時に JSON.parse を試み、失敗した場合は null を送ります。
  • logLevel: enum
    • ログ出力の詳細度。
    • None / Warning / Verbose など。
    • 開発中は Verbose にしておくとデバッグしやすく、本番では WarningNone にする運用を想定。

TypeScriptコードの実装

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


import {
    _decorator,
    Component,
    Node,
    KeyCode,
    input,
    Input,
    EventKeyboard,
    Collider2D,
    ITriggerEvent,
    log,
    warn,
    error,
} from 'cc';
const { ccclass, property } = _decorator;

enum GroupSignalTriggerType {
    None = 0,
    KeyDown = 1,
    TriggerEnter2D = 2,
}

enum GroupSignalLogLevel {
    None = 0,
    Warning = 1,
    Verbose = 2,
}

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

    @property({
        tooltip: '同じグループとして扱うノードに同じ名前を設定します。\n例: "Enemy", "Ally" など。\n空文字の場合、このノードはグループに参加しません。'
    })
    public groupName: string = 'Enemy';

    @property({
        tooltip: 'true の場合、onLoad 時に自動でグループに登録します。'
    })
    public autoRegister: boolean = true;

    @property({
        tooltip: '通知時に各ノードで呼び出したいメソッド名。\n例: "onGroupAlert", "onGroupRetreat" など。\nこの名前のメソッドを任意のコンポーネントに実装しておいてください。'
    })
    public signalMethodName: string = 'onGroupSignal';

    @property({
        tooltip: 'true の場合、自分自身にも通知を送ります。\nfalse の場合、自ノードは通知対象から除外されます。'
    })
    public includeSelf: boolean = false;

    @property({
        tooltip: 'true の場合、start() のタイミングで自動的に sendSignal() を呼びます。'
    })
    public sendOnStart: boolean = false;

    @property({
        type: GroupSignalTriggerType,
        tooltip: '通知を発火させるトリガー種別です。\nNone: 自動では送信せず、他コンポーネントやボタンから sendSignal() を呼びます。\nKeyDown: 指定キーが押されたら送信します。\nTriggerEnter2D: 2Dトリガーに何かが侵入したら送信します。'
    })
    public triggerType: GroupSignalTriggerType = GroupSignalTriggerType.None;

    @property({
        type: KeyCode,
        tooltip: 'triggerType が KeyDown のときに使用するキーコードです。\n例: KEY_E なら E キーで通知します。'
    })
    public keyCode: KeyCode = KeyCode.KEY_E;

    @property({
        tooltip: 'triggerType が TriggerEnter2D のときに使用するフィルタ文字列です。\n空文字なら、どのノードが侵入しても通知します。\n空でない場合、侵入ノードの name がこの文字列と一致したときのみ通知します。'
    })
    public requireTagOnCollider: string = '';

    @property({
        tooltip: '通知時に一緒に送りたい任意データを JSON 文字列で指定します。\n例: {"alertLevel":2,"reason":"player_spotted"}\nJSON のパースに失敗した場合は null が渡されます。'
    })
    public payloadJson: string = '';

    @property({
        type: GroupSignalLogLevel,
        tooltip: 'ログ出力の詳細度を指定します。\nNone: ログを出しません(致命的エラーは除く)。\nWarning: 警告のみ出します。\nVerbose: 送信・受信などの詳細ログも出します。'
    })
    public logLevel: GroupSignalLogLevel = GroupSignalLogLevel.Warning;

    // ---- 静的なグループ管理 ----
    private static _groups: Map<string, Set<GroupSignaler>> = new Map();

    // --- 内部キャッシュ ---
    private _collider2D: Collider2D | null = null;
    private _isRegistered = false;

    // =========================
    // ライフサイクル
    // =========================

    onLoad() {
        if (this.autoRegister) {
            this.registerToGroup();
        }

        // トリガー種別に応じてリスナー登録
        if (this.triggerType === GroupSignalTriggerType.KeyDown) {
            input.on(Input.EventType.KEY_DOWN, this._onKeyDown, this);
        } else if (this.triggerType === GroupSignalTriggerType.TriggerEnter2D) {
            this._setupCollider2D();
        }
    }

    start() {
        if (this.sendOnStart) {
            this.sendSignal();
        }
    }

    onDestroy() {
        this.unregisterFromGroup();

        if (this.triggerType === GroupSignalTriggerType.KeyDown) {
            input.off(Input.EventType.KEY_DOWN, this._onKeyDown, this);
        } else if (this.triggerType === GroupSignalTriggerType.TriggerEnter2D) {
            if (this._collider2D) {
                this._collider2D.off('onTriggerEnter', this._onTriggerEnter, this);
            }
        }
    }

    // =========================
    // グループ登録 / 解除
    // =========================

    private registerToGroup() {
        if (!this.groupName || this.groupName.trim().length === 0) {
            this._logWarning('groupName が空のため、グループに登録されません。');
            return;
        }

        if (this._isRegistered) {
            return;
        }

        let set = GroupSignaler._groups.get(this.groupName);
        if (!set) {
            set = new Set<GroupSignaler>();
            GroupSignaler._groups.set(this.groupName, set);
        }
        set.add(this);
        this._isRegistered = true;

        this._logVerbose(`グループ "${this.groupName}" に登録されました。現在のメンバー数: ${set.size}`);
    }

    private unregisterFromGroup() {
        if (!this._isRegistered) {
            return;
        }
        if (!this.groupName) {
            return;
        }
        const set = GroupSignaler._groups.get(this.groupName);
        if (set) {
            set.delete(this);
            this._logVerbose(`グループ "${this.groupName}" から解除されました。残りメンバー数: ${set.size}`);
            if (set.size === 0) {
                GroupSignaler._groups.delete(this.groupName);
            }
        }
        this._isRegistered = false;
    }

    // =========================
    // トリガーセットアップ
    // =========================

    private _setupCollider2D() {
        this._collider2D = this.getComponent(Collider2D);
        if (!this._collider2D) {
            this._logWarning('triggerType が TriggerEnter2D ですが、このノードに Collider2D がありません。トリガーは動作しません。');
            return;
        }
        if (!this._collider2D.isTrigger) {
            this._logWarning('Collider2D が isTrigger=false です。TriggerEnter2D としては動作しません。isTrigger を true に設定してください。');
        }
        this._collider2D.on('onTriggerEnter', this._onTriggerEnter, this);
    }

    // =========================
    // 入力 / コリジョンイベント
    // =========================

    private _onKeyDown(event: EventKeyboard) {
        if (event.keyCode === this.keyCode) {
            this._logVerbose(`KeyDown トリガー: ${KeyCode[this.keyCode]} が押されました。グループに通知します。`);
            this.sendSignal();
        }
    }

    private _onTriggerEnter(event: ITriggerEvent) {
        const otherNode = event.otherCollider?.node;
        if (!otherNode) {
            return;
        }

        if (this.requireTagOnCollider && otherNode.name !== this.requireTagOnCollider) {
            this._logVerbose(`TriggerEnter2D: 侵入ノード "${otherNode.name}" は requireTag "${this.requireTagOnCollider}" と一致しないため無視しました。`);
            return;
        }

        this._logVerbose(`TriggerEnter2D: ノード "${otherNode.name}" が侵入しました。グループに通知します。`);
        this.sendSignal();
    }

    // =========================
    // パブリック API
    // =========================

    /**
     * 同じ groupName を持つ GroupSignaler 全てに通知を送ります。
     * 他のコンポーネントや UI ボタンのクリックイベントからも自由に呼び出せます。
     */
    public sendSignal() {
        if (!this.groupName || this.groupName.trim().length === 0) {
            this._logWarning('groupName が空のため、sendSignal() は何も行いません。');
            return;
        }
        if (!this.signalMethodName || this.signalMethodName.trim().length === 0) {
            this._logWarning('signalMethodName が空のため、呼び出すメソッドがありません。');
            return;
        }

        const set = GroupSignaler._groups.get(this.groupName);
        if (!set || set.size === 0) {
            this._logWarning(`グループ "${this.groupName}" に登録されたメンバーがいません。通知は行われません。`);
            return;
        }

        let payload: any = null;
        if (this.payloadJson && this.payloadJson.trim().length > 0) {
            try {
                payload = JSON.parse(this.payloadJson);
            } catch (e) {
                this._logWarning(`payloadJson の JSON パースに失敗しました。null を渡します。内容: ${this.payloadJson}`);
                payload = null;
            }
        }

        this._logVerbose(`グループ "${this.groupName}" に対してメソッド "${this.signalMethodName}" をブロードキャストします。メンバー数: ${set.size}`);

        const senderNode = this.node;

        set.forEach((member) => {
            if (!this.includeSelf && member === this) {
                return;
            }
            const targetNode = member.node;
            // 対象ノードにアタッチされている全コンポーネントを走査し、該当メソッドがあれば呼び出す
            const components = targetNode.getComponents(Component);
            for (const comp of components) {
                const handler: any = (comp as any)[this.signalMethodName];
                if (typeof handler === 'function') {
                    try {
                        // 引数は (senderNode, payload) の 2 つを渡す想定
                        handler.call(comp, senderNode, payload);
                        this._logVerbose(`ノード "${targetNode.name}" のコンポーネント "${comp.constructor.name}" に対して "${this.signalMethodName}" を呼び出しました。`);
                    } catch (err) {
                        error(`[GroupSignaler] メソッド "${this.signalMethodName}" の呼び出し中にエラーが発生しました。ノード: ${targetNode.name}, エラー:`, err);
                    }
                }
            }
        });
    }

    // =========================
    // ログ出力ヘルパー
    // =========================

    private _logVerbose(message: string) {
        if (this.logLevel === GroupSignalLogLevel.Verbose) {
            log(`[GroupSignaler][Verbose][${this.node.name}] ${message}`);
        }
    }

    private _logWarning(message: string) {
        if (this.logLevel === GroupSignalLogLevel.Warning || this.logLevel === GroupSignalLogLevel.Verbose) {
            warn(`[GroupSignaler][Warning][${this.node.name}] ${message}`);
        }
    }
}

コードのポイント解説

  • 静的マップによるグループ管理
    • private static _groups: Map<string, Set<GroupSignaler>> で、グループ名ごとに全インスタンスを管理しています。
    • onLoad()autoRegistertrue の場合、自動的に registerToGroup() が呼ばれます。
    • onDestroy() で必ず unregisterFromGroup() を呼び、メモリリークやゴミ参照を防ぎます。
  • トリガー種別ごとのイベント登録
    • triggerType === KeyDown の場合、input.on(KEY_DOWN, ...) でキーボード入力を監視します。
    • triggerType === TriggerEnter2D の場合、Collider2D を取得して on('onTriggerEnter', ...) を登録します。
    • Collider2D が見つからない場合や isTriggerfalse の場合は、警告ログを出してユーザーに設定ミスを知らせます。
  • sendSignal()
    • インスペクタ設定に応じて、JSON ペイロードを JSON.parse し、各メンバーに渡します。
    • 対象ノードの全コンポーネントを走査し、signalMethodName と同名の関数を持つものに対して call で実行します。
    • メソッド呼び出し時の引数は (senderNode, payload) の 2 つを渡す想定です。実装側は必要であればこれを受け取ります。
  • 防御的な実装
    • groupNamesignalMethodName が空の場合は、何もせず警告を出します。
    • グループにメンバーがいない場合も警告ログのみで安全に終了します。
    • メソッド呼び出し時に例外が発生しても try/catch で握りつぶさず、error() ログを出して他のメンバーへの通知は続行します。

使用手順と動作確認

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

  1. Creator の Assets パネルで任意のフォルダを右クリックします。
  2. Create → TypeScript を選択し、ファイル名を GroupSignaler.ts にします。
  3. 自動生成された中身をすべて削除し、本記事の GroupSignaler コードをそのまま貼り付けて保存します。

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

ここでは「Enemy グループに属する 3 体の敵ノード」と、「プレイヤーが近づくと敵全員に警戒通知を送るトリガー」を例にします。

2-1. 敵ノードの作成

  1. Hierarchy パネルで空のノードを 3 つ作成します。
    • 右クリック → Create → Create Empty Node
    • それぞれ Enemy1Enemy2Enemy3 などにリネームします。
  2. 各敵ノードに見た目用の Sprite などを追加しておくと分かりやすいです(任意)。

2-2. 敵の受信メソッドを実装するコンポーネント

敵側が受信するメソッドを実装するため、簡単なコンポーネントを作ります。

  1. Assets パネルで右クリック → Create → TypeScript を選択し、EnemyGroupReceiver.ts を作成します。
  2. 中身を以下のように書き換えます。

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

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

    /**
     * GroupSignaler から呼び出される想定のメソッド。
     * メソッド名は GroupSignaler.signalMethodName と一致させてください。
     */
    public onGroupAlert(sender: Node, payload: any) {
        log(`[EnemyGroupReceiver][${this.node.name}] グループ通知を受信: sender=${sender.name}, payload=${JSON.stringify(payload)}`);
        // ここに「警戒状態に入る」などの処理を書く
    }
}

このコンポーネントは GroupSignaler に依存していない ため、他の用途にも再利用可能です。

2-3. 敵ノードにコンポーネントをアタッチ

  1. HierarchyEnemy1 を選択します。
  2. InspectorAdd Component → Custom → EnemyGroupReceiver を選択して追加します。
  3. 同様に Enemy2Enemy3 にも EnemyGroupReceiver を追加します。

3. GroupSignaler を敵ノードにアタッチしてグループ化

  1. HierarchyEnemy1 を選択します。
  2. InspectorAdd Component → Custom → GroupSignaler を選択して追加します。
  3. GroupSignaler のプロパティを以下のように設定します。
    • Group NamegroupName): Enemy
    • Auto Register: (true)
    • Signal Method Name: onGroupAlert(敵側で実装したメソッド名)
    • Include Self: (テストのため自分も含める)
    • Send On Start: (false)
    • Trigger Type: KeyDown
    • Key Code: KEY_E(プルダウンから選択)
    • Require Tag On Collider: 空のまま(今回は未使用)
    • Payload Json: {"alertLevel":1,"reason":"test_key"}
    • Log Level: Verbose
  4. Enemy2Enemy3 にも同様に GroupSignaler を追加し、同じ設定(特に groupName = "Enemy"signalMethodName = "onGroupAlert")にします。

これで、3 体の敵ノードはすべて「Enemy グループ」として登録され、onGroupAlert メソッドを受け取る準備ができました。

4. キー入力でグループ通知をテストする

  1. シーンを保存し、Play(再生)ボタンでゲームを実行します。
  2. ゲームウィンドウがアクティブになっている状態で、キーボードの E キー を押します。
  3. Console に以下のようなログが 3 行(敵 3 体分)出力されていれば成功です。
    • [EnemyGroupReceiver][Enemy1] グループ通知を受信: sender=Enemy1, payload={"alertLevel":1,"reason":"test_key"}
    • [EnemyGroupReceiver][Enemy2] グループ通知を受信: sender=Enemy1, payload={"alertLevel":1,"reason":"test_key"}
    • [EnemyGroupReceiver][Enemy3] グループ通知を受信: sender=Enemy1, payload={"alertLevel":1,"reason":"test_key"}

この例では、E キーを押したノード(ここでは Enemy1) が sender となって、同じグループのメンバー全員にメソッド呼び出しが飛んでいます。

5. TriggerEnter2D を使った「プレイヤー接近で一斉警戒」の例

次に、プレイヤーが特定の範囲に入ったら Enemy グループ全員に「alertLevel: 2」で通知するケースを作ります。

5-1. プレイヤートリガー用ノードの作成

  1. Hierarchy で右クリック → Create → Create Empty Node を選択し、PlayerAlertTrigger と名付けます。
  2. PlayerAlertTriggerCollider2D(例:CircleCollider2D)を追加します。
    • Add Component → Physics2D → CircleCollider2D など。
    • Is Trigger にチェックを入れて true にします。
    • 半径や位置を調整して、プレイヤーが入ってきそうな範囲に配置します。

5-2. PlayerAlertTrigger に GroupSignaler をアタッチ

  1. InspectorPlayerAlertTrigger を選択し、Add Component → Custom → GroupSignaler を追加します。
  2. プロパティを以下のように設定します。
    • Group Name: Enemy(敵と同じグループ名)
    • Auto Register:
    • Signal Method Name: onGroupAlert(敵側のメソッド名)
    • Include Self: (トリガーノード自身は Enemy グループではないので不要)
    • Send On Start:
    • Trigger Type: TriggerEnter2D
    • Require Tag On Collider: 空のまま(誰が入っても発火)
    • Payload Json: {"alertLevel":2,"reason":"player_entered_zone"}
    • Log Level: Verbose

5-3. プレイヤーノードの作成と動作確認

  1. HierarchyCreate → Create Empty Node を選択し、Player と名付けます。
  2. PlayerRigidBody2DCollider2D(例:CircleCollider2D)を追加します。
    • Collider2D の Is Triggerfalse でも構いません(PlayerAlertTrigger 側がトリガーなので)。
  3. プレイヤーをキーボード操作で動かす簡易スクリプトを用意してもよいですが、まずは手動で配置して 再生中に Scene ビューで動かす だけでもテスト可能です。
  4. ゲームを再生し、Player を Scene ビュー上でドラッグして PlayerAlertTrigger のトリガー範囲に入れてみます。
  5. Console に以下のようなログが 3 行出力されれば成功です。
    • [EnemyGroupReceiver][Enemy1] グループ通知を受信: sender=PlayerAlertTrigger, payload={"alertLevel":2,"reason":"player_entered_zone"}
    • [EnemyGroupReceiver][Enemy2] グループ通知を受信: sender=PlayerAlertTrigger, payload={"alertLevel":2,"reason":"player_entered_zone"}
    • [EnemyGroupReceiver][Enemy3] グループ通知を受信: sender=PlayerAlertTrigger, payload={"alertLevel":2,"reason":"player_entered_zone"}

まとめ

  • GroupSignaler は、同じグループ名を共有するノード同士で簡単に一斉通知ができる 汎用コンポーネントです。
  • インスペクタで以下を設定するだけで使えます:
    • グループ名(groupName
    • 呼び出したいメソッド名(signalMethodName
    • トリガー種別(キー入力 / トリガー侵入 / 手動)
    • 任意の JSON ペイロード
  • 外部の GameManager やシングルトンに一切依存せず、このスクリプト単体で完結しているため、どのプロジェクトにもそのまま持ち込んで再利用できます。
  • 応用例:
    • 敵グループへの「一斉退避」「一斉攻撃」指示
    • 味方ユニットへの「バフ開始」「バフ終了」通知
    • 複数の UI 要素へのテーマ変更通知(ダークモード ON/OFF など)
    • 特定エリアに入ったときの「環境オブジェクト一斉アニメーション開始」など

シンプルな設計ですが、「グループ名+メソッド名+JSON ペイロード」という汎用的な仕組みにしておくことで、多くのゲームロジックを 疎結合なメッセージ駆動 に置き換えられます。
ノードにアタッチしてプロパティを設定するだけで使えるため、レベルデザイナーやスクリプト初心者でも扱いやすいのも利点です。

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