【Cocos Creator 3.8】GlobalEventBus の実装:アタッチするだけで「どのノードからでもグローバルイベント送受信」を実現する汎用スクリプト
このガイドでは、Cocos Creator 3.8.7 + TypeScript で、どのノードからでもイベントを発行・購読できる「グローバルイベントバス」を実装します。
UI ボタンから Player ノードに通知したり、ゲーム内のどこからでも「ゲームポーズ」「スコア更新」などのイベントをやり取りできるようになります。
本コンポーネントは、シーン内の任意の 1 ノードにアタッチするだけで機能し、他のカスタムスクリプトへの依存は一切ありません。
さらに、インスペクタからイベント名の一覧管理や、ログ出力のオン・オフを切り替えられるように設計します。
コンポーネントの設計方針
1. 要件整理
- シーン内に 1 つだけ存在する「グローバルイベントハブ」として機能する。
- 任意のスクリプトから、
GlobalEventBusにアクセスしてイベントを 発行(emit) / 購読(on/off) できる。 - 外部の GameManager やシングルトンに依存せず、このコンポーネントだけで完結する。
- シーンに複数存在した場合は、最初にアクティブになったものを「メイン」として採用し、以降の生成は警告ログを出す。
- インスペクタから以下を設定可能にする:
- デバッグログの出力 ON/OFF
- イベント名のプリセット一覧(補助的な管理用・実行ロジックに必須ではない)
- シーン開始時に、プリセットイベント名をコンソールに一覧表示するかどうか
- イベントの実体は
EventTargetで実装し、TypeScript から静的メソッドで扱える API を提供する。
2. 外部依存をなくすためのアプローチ
- 静的プロパティを使って「現在有効な GlobalEventBus インスタンス」を保持する。
- 任意のスクリプトから
GlobalEventBus.emit(...)などの static メソッドで利用できるようにする。 - シーンロード時に、自身を
_instanceに登録し、onDestroyで解除する。 - インスペクタでの設定値は、
onLoadで静的フィールドに反映させる。 - 依存コンポーネントは特にないが、「シーンに 1 つだけ置くこと」をログで強調する。
3. インスペクタで設定可能なプロパティ
- enableDebugLog: boolean
- 説明: イベント発行・購読・解除・インスタンス切り替え時に、
console.log / warn / errorを出力するかどうか。 - 用途: トラブルシュートやイベントフローの確認用。
- 推奨値: 開発中は
true、リリース時はfalse。
- 説明: イベント発行・購読・解除・インスタンス切り替え時に、
- logPresetEventsOnStart: boolean
- 説明:
start()時に、プリセットイベント名一覧をログ出力するかどうか。 - 用途: プロジェクト全体で使うイベント名を確認しやすくする。
- 説明:
- presetEventNames: string[]
- 説明: よく使うイベント名を一覧で管理するための配列。
- 用途: チーム内での「イベント名の統一」や、間違ったスペルを防ぐためのメモとして利用。
- 備考: あくまで管理用であり、ここに登録しないイベント名も自由に使える。
4. 提供する API(静的メソッド)
どのスクリプトからでも、以下のように使えることを目指します。
// 例: イベント発行(シグナル送信)
GlobalEventBus.emit('PLAYER_DAMAGED', { amount: 10 });
// 例: イベント購読(シグナル受信)
GlobalEventBus.on('PLAYER_DAMAGED', (data) => {
console.log('Player damaged by', data.amount);
}, this);
// 例: イベント購読解除
GlobalEventBus.off('PLAYER_DAMAGED', handler, this);
// 例: 一度だけ受信するイベント
GlobalEventBus.once('GAME_STARTED', () => {
console.log('Game started!');
}, this);
- on(eventName, callback, target?):
- 指定イベントを購読する。
EventTarget.onのラッパー。
- 指定イベントを購読する。
- once(eventName, callback, target?):
- 1 回だけ呼び出される購読。
- off(eventName, callback, target?):
- 購読解除。
- emit(eventName, …args):
- イベント発行。任意の引数をリスナーに渡せる。
- hasInstance(): boolean:
- 現在有効な GlobalEventBus インスタンスがあるかどうか。
- getInstanceNode(): Node | null:
- 現在のインスタンスがアタッチされている Node を返す(デバッグ用)。
TypeScriptコードの実装
import { _decorator, Component, Node, EventTarget, game } from 'cc';
const { ccclass, property } = _decorator;
/**
* GlobalEventBus
* シーン内のどこからでも利用できるグローバルイベントハブ。
* - シーンに 1 つだけ配置することを推奨
* - 他スクリプトから static メソッドでアクセス
*/
@ccclass('GlobalEventBus')
export class GlobalEventBus extends Component {
// ===== インスペクタ設定 =====
@property({
tooltip: 'イベント発行・購読・解除・インスタンス切り替え時にデバッグログを出力するかどうか。\n開発中は ON、リリース時は OFF 推奨。'
})
public enableDebugLog: boolean = true;
@property({
tooltip: 'シーン開始時(start)に、プリセットイベント名一覧をログ出力するかどうか。'
})
public logPresetEventsOnStart: boolean = true;
@property({
type: [String],
tooltip: 'よく使うイベント名を一覧で管理するための配列。\nここに登録していないイベント名も自由に使用可能です。'
})
public presetEventNames: string[] = [
'GAME_START',
'GAME_OVER',
'PLAYER_DAMAGED',
'PLAYER_HEALED',
'SCORE_UPDATED',
];
// ===== 内部静的フィールド =====
/** 現在アクティブな GlobalEventBus インスタンス */
private static _instance: GlobalEventBus | null = null;
/** 実際のイベントディスパッチャ */
private static _eventTarget: EventTarget = new EventTarget();
/** デバッグログ有効フラグ(インスタンスから反映) */
private static _debug: boolean = true;
// ===== ライフサイクル =====
protected onLoad() {
// すでにインスタンスが存在する場合は警告を出す
if (GlobalEventBus._instance && GlobalEventBus._instance !== this) {
console.warn(
'[GlobalEventBus] すでに別のインスタンスが存在します。このノード上の GlobalEventBus はサブインスタンスとして動作します。',
'\n既存インスタンスノード名:', GlobalEventBus._instance.node.name,
'\nこのインスタンスノード名:', this.node.name
);
} else if (!GlobalEventBus._instance) {
// 最初のインスタンスとして登録
GlobalEventBus._instance = this;
if (this.enableDebugLog) {
console.log('[GlobalEventBus] メインインスタンスとして登録されました。ノード:', this.node.name);
}
}
// インスペクタ設定を静的フィールドに反映
GlobalEventBus._debug = this.enableDebugLog;
// シーン変更時に自動的に破棄されるようにする(デフォルト挙動)
// 必要に応じて game.addPersistRootNode(this.node) で永続化も可能だが、
// 本コンポーネントではデフォルト挙動を維持する。
}
protected start() {
if (GlobalEventBus._instance === this) {
if (this.logPresetEventsOnStart && this.presetEventNames.length > 0) {
console.log('[GlobalEventBus] プリセットイベント名一覧:');
this.presetEventNames.forEach((name, index) => {
console.log(` [${index}] ${name}`);
});
}
}
}
protected onEnable() {
if (GlobalEventBus._instance === this && GlobalEventBus._debug) {
console.log('[GlobalEventBus] 有効化されました。ノード:', this.node.name);
}
}
protected onDisable() {
if (GlobalEventBus._instance === this && GlobalEventBus._debug) {
console.log('[GlobalEventBus] 無効化されました。ノード:', this.node.name);
}
}
protected onDestroy() {
if (GlobalEventBus._instance === this) {
if (GlobalEventBus._debug) {
console.log('[GlobalEventBus] メインインスタンスが破棄されました。ノード:', this.node.name);
}
GlobalEventBus._instance = null;
// EventTarget は static なのでそのまま残るが、
// 次のシーンで新しい GlobalEventBus が onLoad されるまで
// hasInstance() は false を返す。
}
}
// ====== 静的ユーティリティ ======
/**
* 現在有効な GlobalEventBus インスタンスが存在するかどうか。
*/
public static hasInstance(): boolean {
return !!GlobalEventBus._instance;
}
/**
* 現在のインスタンスがアタッチされている Node を取得(デバッグ用)。
*/
public static getInstanceNode(): Node | null {
return GlobalEventBus._instance ? GlobalEventBus._instance.node : null;
}
/**
* デバッグログを出力(内部用)。
*/
private static _log(...args: any[]) {
if (!GlobalEventBus._debug) {
return;
}
console.log('[GlobalEventBus]', ...args);
}
private static _warn(...args: any[]) {
if (!GlobalEventBus._debug) {
return;
}
console.warn('[GlobalEventBus]', ...args);
}
private static _error(...args: any[]) {
console.error('[GlobalEventBus]', ...args);
}
/**
* インスタンスが存在しない場合に警告を出す(emit/on/off/once から利用)。
*/
private static _ensureInstance(): boolean {
if (!GlobalEventBus._instance) {
GlobalEventBus._warn(
'有効な GlobalEventBus インスタンスが存在しません。',
'シーン内の任意のノードに GlobalEventBus コンポーネントを 1 つ追加してください。'
);
return false;
}
return true;
}
// ====== 公開 API(どこからでも呼べる) ======
/**
* イベントを購読する。
* @param eventName イベント名
* @param callback コールバック関数
* @param target this として扱うオブジェクト(任意)
*/
public static on(eventName: string, callback: (...args: any[]) => void, target?: any) {
if (!GlobalEventBus._ensureInstance()) {
return;
}
GlobalEventBus._eventTarget.on(eventName, callback, target);
GlobalEventBus._log('on - イベント購読:', eventName, 'target:', target);
}
/**
* 一度だけ呼び出されるイベント購読。
* @param eventName イベント名
* @param callback コールバック関数
* @param target this として扱うオブジェクト(任意)
*/
public static once(eventName: string, callback: (...args: any[]) => void, target?: any) {
if (!GlobalEventBus._ensureInstance()) {
return;
}
GlobalEventBus._eventTarget.once(eventName, callback, target);
GlobalEventBus._log('once - 一度だけのイベント購読:', eventName, 'target:', target);
}
/**
* イベント購読を解除する。
* @param eventName イベント名
* @param callback コールバック関数
* @param target this として扱うオブジェクト(任意)
*/
public static off(eventName: string, callback?: (...args: any[]) => void, target?: any) {
if (!GlobalEventBus._ensureInstance()) {
return;
}
GlobalEventBus._eventTarget.off(eventName, callback, target);
GlobalEventBus._log('off - イベント購読解除:', eventName, 'target:', target);
}
/**
* イベントを発行する。
* 任意の引数をリスナーに渡すことができる。
* @param eventName イベント名
* @param args リスナーに渡す引数
*/
public static emit(eventName: string, ...args: any[]) {
if (!GlobalEventBus._ensureInstance()) {
return;
}
GlobalEventBus._log('emit - イベント発行:', eventName, 'args:', args);
GlobalEventBus._eventTarget.emit(eventName, ...args);
}
}
コードの要点解説
- onLoad:
- すでに他の
GlobalEventBusインスタンスが存在する場合は警告ログ。 - 最初にロードされたインスタンスを
_instanceに登録し、インスペクタのenableDebugLogを静的フィールドに反映。
- すでに他の
- start:
logPresetEventsOnStartが true かつ、プリセットイベントが 1 つ以上あれば、一覧をログ出力。
- onDestroy:
- 自分がメインインスタンスであれば、破棄時に
_instanceをnullに戻す。
- 自分がメインインスタンスであれば、破棄時に
- EventTarget の利用:
_eventTargetは static なEventTargetとして 1 つだけ生成。on/once/off/emitはすべてこの_eventTargetに委譲。
- 防御的実装:
- インスタンスが存在しない状態で
emit/on/off/onceが呼ばれた場合、_ensureInstance()が警告ログを出し、処理をスキップ。 - ログに「シーン内の任意のノードに GlobalEventBus を 1 つ追加してください」と明示。
- インスタンスが存在しない状態で
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ左下の Assets パネルで、スクリプトを配置したいフォルダ(例:
assets/scripts)を選択します。 - そのフォルダ上で右クリックし、Create → TypeScript を選択します。
- 新しく作成されたファイル名を
GlobalEventBus.tsに変更します。 - ダブルクリックしてエディタ(VSCode など)で開き、既存のテンプレートコードをすべて削除して、
上記のGlobalEventBusの TypeScript コードをそのまま貼り付けて保存します。
2. シーンに GlobalEventBus ノードを配置
- Hierarchy パネルで右クリックし、Create → Empty Node を選択します。
- 作成されたノードの名前を
GlobalEventBusNodeなど、分かりやすい名前に変更します。 - そのノードを選択した状態で、右側の Inspector パネルを確認します。
- Add Component ボタンをクリックし、Custom → GlobalEventBus を選択してアタッチします。
- インスペクタに以下のプロパティが表示されていることを確認します:
- Enable Debug Log(デバッグログ ON/OFF)
- Log Preset Events On Start(起動時にプリセットイベント名一覧を出力)
- Preset Event Names(イベント名の配列)
このノードは、どのシーンにも 1 つだけ配置することを推奨します。
もし複数置いてしまった場合は、コンソールに警告が出ますが、最初にロードされたものがメインとして動作します。
3. UI ボタンからイベントを発行する例
次に、UI ボタンから「PLAYER_DAMAGED」イベントを発行してみます。
- Hierarchy パネルで、Canvas 内に Button を 1 つ作成します。
- 右クリック → Create → UI → Button
- Assets パネルで、
PlayerDamageButton.tsという名前の TypeScript スクリプトを作成します。 - 以下のコードを貼り付けます:
import { _decorator, Component } from 'cc';
import { GlobalEventBus } from './GlobalEventBus';
const { ccclass } = _decorator;
@ccclass('PlayerDamageButton')
export class PlayerDamageButton extends Component {
// Button コンポーネントの Click Event から呼ばれるメソッド
public onClickDamage() {
// ダメージ量 10 を送信する例
GlobalEventBus.emit('PLAYER_DAMAGED', { amount: 10 });
}
}
- Hierarchy で先ほど作成した Button ノードを選択し、Inspector の Add Component → Custom → PlayerDamageButton を選択してアタッチします。
- Button コンポーネントの Click Events 設定で:
- 「+」ボタンを押してイベントを 1 つ追加します。
- 追加されたイベントの Node 欄に、Button ノードをドラッグ&ドロップします。
- Component ドロップダウンから PlayerDamageButton を選択します。
- Handler ドロップダウンから onClickDamage を選択します。
これで、ゲーム実行中にこのボタンをクリックすると、
GlobalEventBus.emit('PLAYER_DAMAGED', { amount: 10 }) が実行されるようになります。
4. Player 側でイベントを受信する例
次に、Player ノード側で「PLAYER_DAMAGED」イベントを購読し、ログを出すコンポーネントを作ります。
- Hierarchy で Player 用のノード(例: Player)を作成します。
- 右クリック → Create → 3D Object → Node(または 2D 用の Sprite など)
- Assets パネルで、
PlayerDamageListener.tsという TypeScript スクリプトを作成します。 - 以下のコードを貼り付けます:
import { _decorator, Component } from 'cc';
import { GlobalEventBus } from './GlobalEventBus';
const { ccclass } = _decorator;
@ccclass('PlayerDamageListener')
export class PlayerDamageListener extends Component {
// イベントハンドラをプロパティとして保持しておくと off しやすい
private _onDamaged = (data: { amount: number }) => {
console.log('[PlayerDamageListener] PLAYER_DAMAGED 受信:', data.amount);
// ここで実際に HP を減らしたりする処理を書く
};
protected onEnable() {
// イベント購読
GlobalEventBus.on('PLAYER_DAMAGED', this._onDamaged, this);
}
protected onDisable() {
// イベント購読解除(メモリリーク防止)
GlobalEventBus.off('PLAYER_DAMAGED', this._onDamaged, this);
}
}
- Hierarchy で Player ノードを選択し、Inspector から Add Component → Custom → PlayerDamageListener を追加します。
これで、ボタンを押す → GlobalEventBus がイベント発行 → PlayerDamageListener が受信という流れが完成します。
5. 実行して動作確認
- GlobalEventBusNode にアタッチした GlobalEventBus のインスペクタで:
- Enable Debug Log を
trueに設定。 - Log Preset Events On Start も
trueのままにします。
- Enable Debug Log を
- メニューから Project → Preview でブラウザプレビュー(または上部の再生ボタン)を実行します。
- コンソールログを確認すると:
[GlobalEventBus] メインインスタンスとして登録されました。ノード: GlobalEventBusNode[GlobalEventBus] プリセットイベント名一覧:などが表示されます。
- ゲーム画面で Button をクリックすると:
[GlobalEventBus] emit - イベント発行: PLAYER_DAMAGED args: [{ amount: 10 }][GlobalEventBus] on - イベント購読: PLAYER_DAMAGED target: PlayerDamageListener {...}(起動時に一度)[PlayerDamageListener] PLAYER_DAMAGED 受信: 10
これで、UI ボタンと Player ノードが直接参照し合うことなく、グローバルイベントを介して連携できていることが確認できます。
まとめ
この GlobalEventBus コンポーネントを導入することで:
- UI ↔ Player、敵 ↔ スコア管理、システム ↔ 各種 UI など、関係のないノード同士を疎結合で連携できる。
- どのスクリプトからでも
GlobalEventBus.emit/on/once/offを呼ぶだけでよく、複雑な参照取得や GameManager の設計が不要になる。 - インスペクタの presetEventNames でイベント名を一覧管理でき、チーム開発時のイベント名の乱立やスペルミスを防ぐ助けになる。
- enableDebugLog を使って、開発中はイベントの流れを詳細に追跡し、リリース時にはログをオフにしてパフォーマンスに配慮できる。
本コンポーネントは、1 つのスクリプトファイルと 1 つのノードにアタッチするだけで機能し、他のカスタムシングルトンやマネージャークラスに依存しないため、
どのプロジェクトにも簡単に持ち込める「再利用性の高いユーティリティ」として活用できます。
この GlobalEventBus をベースに、イベント名を enum で型安全に管理する、イベントごとにペイロード型を定義する、シーンをまたいで永続化するなど、
プロジェクトの規模やチームの運用に合わせて拡張していくと、より堅牢で保守しやすいイベント駆動アーキテクチャを構築できます。




