【Cocos Creator 3.8】GroupSignaler の実装:アタッチするだけで「同じグループのノードに一括通知」を実現する汎用スクリプト
本記事では、特定のイベント(例:キー入力・ボタン押下・トリガー侵入など)をきっかけに、同じ「グループ名」を持つノードへ一括でメッセージ(メソッド呼び出し)を送る汎用コンポーネント GroupSignaler を実装します。
敵グループ全員に「警戒開始」「退避開始」などの通知を送りたいとき、各ノードに同じコンポーネントをアタッチし、インスペクタでグループ名とメソッド名を指定するだけで動作します。外部の GameManager やシングルトンには一切依存しません。
コンポーネントの設計方針
機能要件の整理
- 同じ「グループ名」を共有するノード同士で、簡易的なメッセージ通信(メソッド呼び出し)を行いたい。
- 通知のトリガーは、以下のような複数パターンから選べるようにする:
- 手動(他スクリプトから
sendSignal()を呼ぶ) - シーン開始時(
onLoad/start) - キー入力(指定キーが押されたら)
- ボタン(
Buttonコンポーネントのクリックイベントから呼ぶ) - 2Dコリジョンのトリガー侵入時(
Collider2DのonTriggerEnter相当)
- 手動(他スクリプトから
- 通知先は「同じグループ名の
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.nameやgroupなど)を要求したい場合に使用。- 空文字なら無条件でトリガーします。
- 簡易実装として、ここでは 相手ノードの
nameと比較 することにします。
- payloadJson: string
- 通知時に一緒に送りたいデータを JSON 文字列で指定。
- 例:
{"alertLevel":2,"reason":"player_spotted"} - 送信時に
JSON.parseを試み、失敗した場合はnullを送ります。
- logLevel: enum
- ログ出力の詳細度。
None/Warning/Verboseなど。- 開発中は
Verboseにしておくとデバッグしやすく、本番ではWarningかNoneにする運用を想定。
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()でautoRegisterがtrueの場合、自動的にregisterToGroup()が呼ばれます。onDestroy()で必ずunregisterFromGroup()を呼び、メモリリークやゴミ参照を防ぎます。
- トリガー種別ごとのイベント登録
triggerType === KeyDownの場合、input.on(KEY_DOWN, ...)でキーボード入力を監視します。triggerType === TriggerEnter2Dの場合、Collider2Dを取得してon('onTriggerEnter', ...)を登録します。Collider2Dが見つからない場合やisTriggerがfalseの場合は、警告ログを出してユーザーに設定ミスを知らせます。
- sendSignal()
- インスペクタ設定に応じて、JSON ペイロードを
JSON.parseし、各メンバーに渡します。 - 対象ノードの全コンポーネントを走査し、
signalMethodNameと同名の関数を持つものに対してcallで実行します。 - メソッド呼び出し時の引数は
(senderNode, payload)の 2 つを渡す想定です。実装側は必要であればこれを受け取ります。
- インスペクタ設定に応じて、JSON ペイロードを
- 防御的な実装
groupNameやsignalMethodNameが空の場合は、何もせず警告を出します。- グループにメンバーがいない場合も警告ログのみで安全に終了します。
- メソッド呼び出し時に例外が発生しても
try/catchで握りつぶさず、error()ログを出して他のメンバーへの通知は続行します。
使用手順と動作確認
1. スクリプトファイルの作成
- Creator の Assets パネルで任意のフォルダを右クリックします。
- Create → TypeScript を選択し、ファイル名を
GroupSignaler.tsにします。 - 自動生成された中身をすべて削除し、本記事の
GroupSignalerコードをそのまま貼り付けて保存します。
2. テスト用のノードとコンポーネントを用意する
ここでは「Enemy グループに属する 3 体の敵ノード」と、「プレイヤーが近づくと敵全員に警戒通知を送るトリガー」を例にします。
2-1. 敵ノードの作成
- Hierarchy パネルで空のノードを 3 つ作成します。
- 右クリック → Create → Create Empty Node
- それぞれ
Enemy1、Enemy2、Enemy3などにリネームします。
- 各敵ノードに見た目用の Sprite などを追加しておくと分かりやすいです(任意)。
2-2. 敵の受信メソッドを実装するコンポーネント
敵側が受信するメソッドを実装するため、簡単なコンポーネントを作ります。
- Assets パネルで右クリック → Create → TypeScript を選択し、
EnemyGroupReceiver.tsを作成します。 - 中身を以下のように書き換えます。
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. 敵ノードにコンポーネントをアタッチ
- Hierarchy で
Enemy1を選択します。 - Inspector で Add Component → Custom → EnemyGroupReceiver を選択して追加します。
- 同様に
Enemy2、Enemy3にもEnemyGroupReceiverを追加します。
3. GroupSignaler を敵ノードにアタッチしてグループ化
- Hierarchy で
Enemy1を選択します。 - Inspector で Add Component → Custom → GroupSignaler を選択して追加します。
- GroupSignaler のプロパティを以下のように設定します。
- Group Name(
groupName):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
- Group Name(
Enemy2とEnemy3にも同様に GroupSignaler を追加し、同じ設定(特にgroupName = "Enemy"、signalMethodName = "onGroupAlert")にします。
これで、3 体の敵ノードはすべて「Enemy グループ」として登録され、onGroupAlert メソッドを受け取る準備ができました。
4. キー入力でグループ通知をテストする
- シーンを保存し、Play(再生)ボタンでゲームを実行します。
- ゲームウィンドウがアクティブになっている状態で、キーボードの E キー を押します。
- 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. プレイヤートリガー用ノードの作成
- Hierarchy で右クリック → Create → Create Empty Node を選択し、
PlayerAlertTriggerと名付けます。 PlayerAlertTriggerに Collider2D(例:CircleCollider2D)を追加します。- Add Component → Physics2D → CircleCollider2D など。
Is Triggerにチェックを入れて true にします。- 半径や位置を調整して、プレイヤーが入ってきそうな範囲に配置します。
5-2. PlayerAlertTrigger に GroupSignaler をアタッチ
- Inspector で
PlayerAlertTriggerを選択し、Add Component → Custom → GroupSignaler を追加します。 - プロパティを以下のように設定します。
- 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
- Group Name:
5-3. プレイヤーノードの作成と動作確認
- Hierarchy で Create → Create Empty Node を選択し、
Playerと名付けます。 Playerに RigidBody2D と Collider2D(例:CircleCollider2D)を追加します。- Collider2D の
Is Triggerは false でも構いません(PlayerAlertTrigger側がトリガーなので)。
- Collider2D の
- プレイヤーをキーボード操作で動かす簡易スクリプトを用意してもよいですが、まずは手動で配置して 再生中に Scene ビューで動かす だけでもテスト可能です。
- ゲームを再生し、
Playerを Scene ビュー上でドラッグしてPlayerAlertTriggerのトリガー範囲に入れてみます。 - 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 ペイロード」という汎用的な仕組みにしておくことで、多くのゲームロジックを 疎結合なメッセージ駆動 に置き換えられます。
ノードにアタッチしてプロパティを設定するだけで使えるため、レベルデザイナーやスクリプト初心者でも扱いやすいのも利点です。




