【Cocos Creator】アタッチするだけ!GlobalEventBus (イベントバス)の実装方法【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】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:
    • 自分がメインインスタンスであれば、破棄時に _instancenull に戻す。
  • EventTarget の利用:
    • _eventTarget は static な EventTarget として 1 つだけ生成。
    • on/once/off/emit はすべてこの _eventTarget に委譲。
  • 防御的実装:
    • インスタンスが存在しない状態で emit/on/off/once が呼ばれた場合、_ensureInstance() が警告ログを出し、処理をスキップ。
    • ログに「シーン内の任意のノードに GlobalEventBus を 1 つ追加してください」と明示。

使用手順と動作確認

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

  1. エディタ左下の Assets パネルで、スクリプトを配置したいフォルダ(例: assets/scripts)を選択します。
  2. そのフォルダ上で右クリックし、Create → TypeScript を選択します。
  3. 新しく作成されたファイル名を GlobalEventBus.ts に変更します。
  4. ダブルクリックしてエディタ(VSCode など)で開き、既存のテンプレートコードをすべて削除して、
    上記の GlobalEventBus の TypeScript コードをそのまま貼り付けて保存します。

2. シーンに GlobalEventBus ノードを配置

  1. Hierarchy パネルで右クリックし、Create → Empty Node を選択します。
  2. 作成されたノードの名前を GlobalEventBusNode など、分かりやすい名前に変更します。
  3. そのノードを選択した状態で、右側の Inspector パネルを確認します。
  4. Add Component ボタンをクリックし、Custom → GlobalEventBus を選択してアタッチします。
  5. インスペクタに以下のプロパティが表示されていることを確認します:
    • Enable Debug Log(デバッグログ ON/OFF)
    • Log Preset Events On Start(起動時にプリセットイベント名一覧を出力)
    • Preset Event Names(イベント名の配列)

このノードは、どのシーンにも 1 つだけ配置することを推奨します。
もし複数置いてしまった場合は、コンソールに警告が出ますが、最初にロードされたものがメインとして動作します。

3. UI ボタンからイベントを発行する例

次に、UI ボタンから「PLAYER_DAMAGED」イベントを発行してみます。

  1. Hierarchy パネルで、Canvas 内に Button を 1 つ作成します。
    • 右クリック → Create → UI → Button
  2. Assets パネルで、PlayerDamageButton.ts という名前の TypeScript スクリプトを作成します。
  3. 以下のコードを貼り付けます:

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 });
    }
}
  1. Hierarchy で先ほど作成した Button ノードを選択し、Inspector の Add Component → Custom → PlayerDamageButton を選択してアタッチします。
  2. Button コンポーネントの Click Events 設定で:
    1. 「+」ボタンを押してイベントを 1 つ追加します。
    2. 追加されたイベントの Node 欄に、Button ノードをドラッグ&ドロップします。
    3. Component ドロップダウンから PlayerDamageButton を選択します。
    4. Handler ドロップダウンから onClickDamage を選択します。

これで、ゲーム実行中にこのボタンをクリックすると、
GlobalEventBus.emit('PLAYER_DAMAGED', { amount: 10 }) が実行されるようになります。

4. Player 側でイベントを受信する例

次に、Player ノード側で「PLAYER_DAMAGED」イベントを購読し、ログを出すコンポーネントを作ります。

  1. Hierarchy で Player 用のノード(例: Player)を作成します。
    • 右クリック → Create → 3D Object → Node(または 2D 用の Sprite など)
  2. Assets パネルで、PlayerDamageListener.ts という TypeScript スクリプトを作成します。
  3. 以下のコードを貼り付けます:

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);
    }
}
  1. Hierarchy で Player ノードを選択し、Inspector から Add Component → Custom → PlayerDamageListener を追加します。

これで、ボタンを押す → GlobalEventBus がイベント発行 → PlayerDamageListener が受信という流れが完成します。

5. 実行して動作確認

  1. GlobalEventBusNode にアタッチした GlobalEventBus のインスペクタで:
    • Enable Debug Logtrue に設定。
    • Log Preset Events On Starttrue のままにします。
  2. メニューから Project → Preview でブラウザプレビュー(または上部の再生ボタン)を実行します。
  3. コンソールログを確認すると:
    • [GlobalEventBus] メインインスタンスとして登録されました。ノード: GlobalEventBusNode
    • [GlobalEventBus] プリセットイベント名一覧: などが表示されます。
  4. ゲーム画面で 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 で型安全に管理するイベントごとにペイロード型を定義するシーンをまたいで永続化するなど、
プロジェクトの規模やチームの運用に合わせて拡張していくと、より堅牢で保守しやすいイベント駆動アーキテクチャを構築できます。

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