【Cocos Creator】アタッチするだけ!ModalWindow (ポップアップ)の実装方法【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】ModalWindow(確認用ポップアップ)の実装:アタッチするだけで「本当に終了しますか?」などの確認ダイアログと背景入力ブロックを実現する汎用スクリプト

このガイドでは、任意のノードにアタッチするだけで「確認用モーダルウィンドウ(ポップアップ)」として動作する汎用コンポーネント ModalWindow を実装します。

  • 「本当に終了しますか?」のような確認メッセージ表示
  • OK / Cancel ボタンのクリック検知
  • モーダル表示中は背景の入力(クリック/タッチ)をブロック
  • 外部スクリプトに依存せず、このコンポーネント単体で完結

といった要件を満たします。UI レイアウト(背景パネルやボタンの見た目)は自由に作ってよく、インスペクタからノード参照と文言を設定するだけで汎用的に使えるように設計します。


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

1. 要件の整理

  • 任意の UI ノードに ModalWindow をアタッチすると、そのノード全体を「モーダルダイアログ」として扱う。
  • モーダル表示中は、このモーダルの外側のクリック/タッチを無効化する(背景入力ブロック)。
  • OK / Cancel ボタンを押したときの処理は、インスペクタからコールバック(Component.method)として設定できるようにする。
  • 外部の GameManager やシングルトンには一切依存しない。
  • UI ノード構成はプロジェクトごとに違うため、必要なボタンやラベルはすべて @property で参照を受け取り、存在しなければ警告を出す。
  • 「モーダルの開閉」は、open(), close() メソッドと、isOpen フラグで管理する。

2. 想定するノード構成

最低限、以下のような構成を想定します(名前は自由ですが、インスペクタで参照を設定します)。

ModalRoot(任意のノード) ← ここに ModalWindow.ts をアタッチ
├─ Background (cc.Sprite / cc.UITransform など)
├─ Panel (任意のレイアウト)
│   ├─ MessageLabel (cc.Label)
│   ├─ OkButton (cc.Button)
│   └─ CancelButton (cc.Button) ※任意

重要:背景入力ブロックのために、ModalRoot には UITransformWidget を使って画面全体を覆うように広げることを推奨します(後述の使用手順で説明)。

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

  • backgroundNode: Node | null
    • モーダルの背景(半透明の黒など)のノード。
    • ここに ButtonBlockInputEvents を付けてもよいですが、本コンポーネント内でイベントをブロックするため、必須ではありません。
    • 未設定でも動作しますが、見た目上の暗転が必要な場合は設定してください。
  • panelNode: Node | null
    • メッセージやボタンを配置したパネルノード。
    • 未設定でもエラーにはしませんが、通常は設定します。
  • messageLabel: Label | null
    • 確認メッセージを表示する Label コンポーネント。
    • 未設定の場合、メッセージの自動変更は行われず、警告ログを出します。
  • defaultMessage: string
    • モーダルを開いたときに表示するデフォルトメッセージ。
    • 例: 「本当に終了しますか?」
  • okButton: Button | null
    • OK 決定用のボタン。
    • 未設定の場合は OK ボタン機能が無効になり、警告ログを出します。
  • cancelButton: Button | null
    • キャンセル用のボタン(任意)。
    • 未設定の場合は Cancel 機能は使われません。
  • closeOnBackground: boolean
    • true の場合、背景部分をクリック/タッチするとモーダルを閉じる。
    • false の場合、背景クリックでは閉じない(厳密な確認ダイアログなどに適用)。
  • autoOpenOnStart: boolean
    • true の場合、start() 時に自動でモーダルを開く。
    • テストやチュートリアル用に便利。
  • blockInputOnModal: boolean
    • true の場合、モーダルが開いている間は画面全体の入力をブロックする。
    • 内部的には Node.EventType.TOUCH_START / MOUSE_DOWN をキャプチャして event.propagationStopped = true する。
  • onOkTarget: Component | null, onOkMethodName: string
    • OK ボタン押下時に呼び出すメソッド。
    • onOkTarget に任意のコンポーネントを指定し、onOkMethodName にそのメソッド名(例: "onConfirmExit")を文字列で指定。
    • メソッドシグネチャは onConfirmExit(): void など引数なしを想定。
  • onCancelTarget: Component | null, onCancelMethodName: string
    • Cancel ボタン押下時に呼び出すメソッド(任意)。
    • 設定されていない場合は何も呼び出さずに閉じるだけ。

これらをすべて @property で公開し、外部スクリプトに依存せずインスペクタから完結するようにします。


TypeScriptコードの実装


import {
    _decorator,
    Component,
    Node,
    Label,
    Button,
    EventTouch,
    EventMouse,
    Event,
    log,
    warn,
} from 'cc';
const { ccclass, property } = _decorator;

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

    @property({
        tooltip: 'モーダルの背景ノード(半透明の黒パネルなど)。\n未設定でも動作しますが、見た目上の暗転が必要な場合は設定してください。',
    })
    public backgroundNode: Node | null = null;

    @property({
        tooltip: 'メッセージやボタンを配置したパネルノード。\n未設定でも動作しますが、通常は設定してください。',
    })
    public panelNode: Node | null = null;

    @property({
        type: Label,
        tooltip: '確認メッセージを表示する Label コンポーネント。\n未設定の場合、メッセージの自動変更は行われません。',
    })
    public messageLabel: Label | null = null;

    @property({
        tooltip: 'モーダルを開いたときに表示するデフォルトメッセージ。',
    })
    public defaultMessage: string = '本当に終了しますか?';

    @property({
        type: Button,
        tooltip: 'OK 決定用のボタン。\n未設定の場合、OK 機能は無効になり警告が出ます。',
    })
    public okButton: Button | null = null;

    @property({
        type: Button,
        tooltip: 'キャンセル用のボタン(任意)。\n未設定の場合、Cancel 機能は使われません。',
    })
    public cancelButton: Button | null = null;

    @property({
        tooltip: 'true の場合、背景部分のクリック/タッチでモーダルを閉じます。',
    })
    public closeOnBackground: boolean = false;

    @property({
        tooltip: 'true の場合、start() 時に自動でモーダルを開きます。',
    })
    public autoOpenOnStart: boolean = false;

    @property({
        tooltip: 'true の場合、モーダルが開いている間は画面全体の入力をブロックします。',
    })
    public blockInputOnModal: boolean = true;

    @property({
        type: Component,
        tooltip: 'OK ボタン押下時に呼び出すターゲットコンポーネント。\n任意のノード上の任意のコンポーネントを指定できます。',
    })
    public onOkTarget: Component | null = null;

    @property({
        tooltip: 'OK ボタン押下時に呼び出すメソッド名(引数なし)。\n例: "onConfirmExit"',
    })
    public onOkMethodName: string = '';

    @property({
        type: Component,
        tooltip: 'Cancel ボタン押下時に呼び出すターゲットコンポーネント(任意)。',
    })
    public onCancelTarget: Component | null = null;

    @property({
        tooltip: 'Cancel ボタン押下時に呼び出すメソッド名(引数なし)。',
    })
    public onCancelMethodName: string = '';

    /** 現在モーダルが開いているかどうか */
    public get isOpen(): boolean {
        return this._isOpen;
    }

    private _isOpen: boolean = false;

    // 入力ブロック用のイベントハンドラを使い回すために保持
    private _touchBlocker: (event: EventTouch) => void;
    private _mouseBlocker: (event: EventMouse) => void;

    constructor() {
        super();
        // アロー関数で this をバインドしておく
        this._touchBlocker = (event: EventTouch) => {
            if (!this._isOpen || !this.blockInputOnModal) {
                return;
            }
            // 背景含め画面全体の入力をブロック
            event.propagationStopped = true;
        };

        this._mouseBlocker = (event: EventMouse) => {
            if (!this._isOpen || !this.blockInputOnModal) {
                return;
            }
            event.propagationStopped = true;
        };
    }

    protected onLoad(): void {
        // 初期状態ではモーダルを閉じておく
        this._setActiveAll(false);

        // OK ボタンのイベント登録
        if (this.okButton) {
            this.okButton.node.on(Button.EventType.CLICK, this._onOkClicked, this);
        } else {
            warn('[ModalWindow] okButton が設定されていません。OK ボタンは動作しません。');
        }

        // Cancel ボタンのイベント登録
        if (this.cancelButton) {
            this.cancelButton.node.on(Button.EventType.CLICK, this._onCancelClicked, this);
        } else {
            // Cancel は任意なので警告は控えめに
            log('[ModalWindow] cancelButton が設定されていません。Cancel ボタンは使用されません。');
        }

        // 背景クリックで閉じるオプション
        if (this.backgroundNode) {
            this.backgroundNode.on(Node.EventType.TOUCH_START, this._onBackgroundTouched, this);
            this.backgroundNode.on(Node.EventType.MOUSE_DOWN, this._onBackgroundTouched, this);
        } else {
            log('[ModalWindow] backgroundNode が設定されていません。背景クリックで閉じる機能は利用できません。');
        }

        // 入力ブロック用イベントをルートノードに登録
        this.node.on(Node.EventType.TOUCH_START, this._touchBlocker, this, true);
        this.node.on(Node.EventType.MOUSE_DOWN, this._mouseBlocker, this, true);
    }

    protected start(): void {
        if (this.autoOpenOnStart) {
            this.open(this.defaultMessage);
        }
    }

    protected onDestroy(): void {
        // イベントのクリーンアップ
        if (this.okButton) {
            this.okButton.node.off(Button.EventType.CLICK, this._onOkClicked, this);
        }
        if (this.cancelButton) {
            this.cancelButton.node.off(Button.EventType.CLICK, this._onCancelClicked, this);
        }
        if (this.backgroundNode) {
            this.backgroundNode.off(Node.EventType.TOUCH_START, this._onBackgroundTouched, this);
            this.backgroundNode.off(Node.EventType.MOUSE_DOWN, this._onBackgroundTouched, this);
        }

        this.node.off(Node.EventType.TOUCH_START, this._touchBlocker, this, true);
        this.node.off(Node.EventType.MOUSE_DOWN, this._mouseBlocker, this, true);
    }

    /**
     * モーダルを開く。
     * @param message 表示するメッセージ。省略時は defaultMessage を使用。
     */
    public open(message?: string): void {
        const msg = message !== undefined ? message : this.defaultMessage;
        this._isOpen = true;
        this._setActiveAll(true);

        if (this.messageLabel) {
            this.messageLabel.string = msg;
        } else {
            warn('[ModalWindow] messageLabel が設定されていないため、メッセージを表示できません。');
        }
    }

    /**
     * モーダルを閉じる。
     */
    public close(): void {
        this._isOpen = false;
        this._setActiveAll(false);
    }

    /**
     * 内部用:OK ボタン押下時の処理。
     */
    private _onOkClicked(): void {
        log('[ModalWindow] OK clicked');
        this._invokeCallback(this.onOkTarget, this.onOkMethodName);
        this.close();
    }

    /**
     * 内部用:Cancel ボタン押下時の処理。
     */
    private _onCancelClicked(): void {
        log('[ModalWindow] Cancel clicked');
        this._invokeCallback(this.onCancelTarget, this.onCancelMethodName);
        this.close();
    }

    /**
     * 内部用:背景がクリック/タッチされたときの処理。
     */
    private _onBackgroundTouched(event: EventTouch | EventMouse): void {
        if (!this._isOpen) {
            return;
        }

        // 入力ブロックとして、ここでイベント伝播を止める
        event.propagationStopped = true;

        if (this.closeOnBackground) {
            this.close();
        }
    }

    /**
     * 指定されたターゲットコンポーネントのメソッドを呼び出す(存在チェック付き)。
     */
    private _invokeCallback(target: Component | null, methodName: string): void {
        if (!target) {
            if (methodName) {
                warn(`[ModalWindow] コールバックターゲットが設定されていません (method: ${methodName})`);
            }
            return;
        }
        if (!methodName) {
            warn('[ModalWindow] メソッド名が設定されていません。');
            return;
        }

        const anyTarget = target as any;
        const func = anyTarget[methodName];

        if (typeof func === 'function') {
            try {
                func.call(target);
            } catch (e) {
                warn(`[ModalWindow] コールバック呼び出し中にエラーが発生しました: ${methodName}`, e);
            }
        } else {
            warn(`[ModalWindow] 指定されたメソッドが見つからないか関数ではありません: ${methodName}`);
        }
    }

    /**
     * モーダル全体の表示/非表示を切り替える。
     */
    private _setActiveAll(active: boolean): void {
        this.node.active = active;
        if (this.backgroundNode) {
            this.backgroundNode.active = active;
        }
        if (this.panelNode) {
            this.panelNode.active = active;
        }
    }
}

コードのポイント解説

  • onLoad()
    • 初期状態ではモーダルを閉じた状態(_setActiveAll(false))。
    • OK / Cancel ボタンにクリックイベントを登録。
    • 背景ノードにタッチ/マウスイベントを登録し、必要に応じて閉じる。
    • 入力ブロック用に、モーダルルートノードに TOUCH_START / MOUSE_DOWN をキャプチャ登録。
  • start()
    • autoOpenOnStarttrue の場合、自動で open() を呼び出し、defaultMessage を表示。
  • open(message?: string)
    • モーダルを開き、渡されたメッセージ(または defaultMessage)をラベルに設定。
    • messageLabel が未設定なら警告ログ。
  • close()
    • モーダル全体を非表示にし、_isOpenfalse に。
  • 入力ブロック
    • this.node.on(Node.EventType.TOUCH_START, this._touchBlocker, this, true) の第4引数 true により、キャプチャフェーズでイベントを受け取ります。
    • モーダルが開いている間は event.propagationStopped = true; として背景への伝播を止めることで、背景入力をブロック。
  • コールバック呼び出し
    • onOkTarget + onOkMethodName / onCancelTarget + onCancelMethodName を組み合わせて、任意のコンポーネントメソッドを呼び出す。
    • メソッドの存在チェックと try-catch による防御的実装。

使用手順と動作確認

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

  1. Assets パネルで右クリック → Create → TypeScript を選択し、ModalWindow.ts という名前で作成します。
  2. 自動生成されたコードをすべて削除し、本記事の ModalWindow のコードを貼り付けて保存します。

2. モーダル用 UI ノードの作成

  1. Hierarchy パネルで右クリック → Create → UI → Canvas (既に Canvas がある場合は再利用)。
  2. Canvas の子として、右クリック → Create → UI → Empty Node を作成し、名前を ModalRoot に変更します。
  3. ModalRoot を選択し、Inspector の Add Component → UI → UITransform を追加します(通常は Canvas の子には既に付いていますが、なければ追加)。
  4. 同じく ModalRootAdd Component → UI → Widget を追加し、Left / Right / Top / Bottom をすべて 0 に設定してチェックを入れ、画面全体を覆うようにします。

3. 背景ノードの作成

  1. ModalRoot を右クリック → Create → UI → Sprite を作成し、Background と命名します。
  2. Background を選択し、Inspector の Sprite で
    • Color を (0, 0, 0, 160〜200) 程度の半透明黒に設定
    • Size Mode を TRIMMED から CUSTOM に変更し、UITransform の Width/Height を画面サイズに合わせる(または Widget コンポーネントで四辺 0 固定)。

4. パネルとボタンの作成

  1. ModalRoot を右クリック → Create → UI → Sprite を作成し、Panel と命名します。
  2. Panel の Sprite で、ダイアログ風の背景画像や単色を設定し、UITransform で適度なサイズ(例: 400×250)に調整します。
  3. Panel を選択 → 右クリック → Create → UI → Label を作成し、MessageLabel と命名します。
    • テキストを仮に 本当に終了しますか? にしておきます。
    • Alignment を中央揃えにし、パネル内で読みやすい位置に配置。
  4. Panel を選択 → 右クリック → Create → UI → Button を作成し、OkButton と命名します。
    • Button の子に自動で Label が付きますので、テキストを OK に変更。
  5. 同様に Panel の子として CancelButton を作成し、テキストを キャンセル に変更します。
  6. OK / Cancel ボタンをパネル下部に左右に配置します。

5. ModalWindow コンポーネントのアタッチと設定

  1. Hierarchy で ModalRoot を選択します。
  2. Inspector の Add Component → Custom → ModalWindow を選択してアタッチします。
  3. Inspector 上の ModalWindow セクションで以下を設定します。
    • Background Node: Background ノードをドラッグ&ドロップ。
    • Panel Node: Panel ノードをドラッグ&ドロップ。
    • Message Label: MessageLabelLabel コンポーネントをドラッグ&ドロップ。
    • Default Message: 本当に終了しますか? など任意の文言。
    • Ok Button: OkButtonButton コンポーネントをドラッグ&ドロップ。
    • Cancel Button: CancelButtonButton コンポーネントをドラッグ&ドロップ。
    • Close On Background: 背景クリックで閉じたい場合は ON
    • Auto Open On Start: テスト用に、起動直後から表示したい場合は ON
    • Block Input On Modal: 通常は ON のまま(背景入力をブロック)。
    • On Ok Target / On Ok Method Name:
      • 例として、Canvas に GameController.ts を作り、そこに onConfirmExit() メソッドを実装しておきます。
      • On Ok Target に Canvas の GameController コンポーネントをドラッグ&ドロップ。
      • On Ok Method NameonConfirmExit と入力。
    • On Cancel Target / On Cancel Method Name:
      • キャンセル時に特別な処理が不要なら空のままで構いません。
      • 何か処理したい場合は、上記 OK と同様に設定します。

6. 動作確認

  1. 再生ボタン(Play)を押してゲームを実行します。
  2. Auto Open On StartON にしている場合、起動直後にモーダルが表示されます。
    • 背景が半透明で暗くなり、パネルとメッセージ、OK / Cancel ボタンが表示されていることを確認します。
  3. OK ボタンをクリックすると:
    • コンソールに [ModalWindow] OK clicked のログが出る。
    • 設定していれば、onOkTarget.onConfirmExit() が呼ばれる。
    • モーダルが閉じて UI が消える。
  4. Cancel ボタンをクリックすると:
    • コンソールに [ModalWindow] Cancel clicked のログが出る。
    • 設定していれば、onCancelTarget.onCancelSomething() が呼ばれる。
    • モーダルが閉じる。
  5. モーダルが開いている状態で、背景部分をクリック/タッチしてみます。
    • Block Input On Modaltrue のため、背景のボタンなどは反応しないことを確認します。
    • Close On Backgroundtrue の場合は、背景クリックでモーダルが閉じることを確認します。

7. スクリプトからモーダルを開く例

他のスクリプトから動的にモーダルを開きたい場合も、ModalWindow 自体は外部に依存していないので、呼び出し側が getComponent するだけで済みます。


// 任意のコンポーネント内の例
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
import { ModalWindow } from './ModalWindow';

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

    @property(Node)
    public modalRootNode: Node | null = null;

    private _modal: ModalWindow | null = null;

    protected start(): void {
        if (this.modalRootNode) {
            this._modal = this.modalRootNode.getComponent(ModalWindow);
            if (!this._modal) {
                console.warn('[GameController] modalRootNode に ModalWindow コンポーネントがアタッチされていません。');
            }
        } else {
            console.warn('[GameController] modalRootNode が設定されていません。');
        }
    }

    public onPauseButtonClicked(): void {
        if (this._modal) {
            this._modal.open('ゲームを中断しますか?');
        }
    }

    public onConfirmExit(): void {
        // OK 押下時の処理例
        console.log('ゲーム終了処理をここに書く');
    }
}

このように、ModalWindow 自体は他のスクリプトに依存せず、呼び出し側が必要に応じて参照するだけの関係になっています。


まとめ

本記事では、Cocos Creator 3.8 / TypeScript で以下を満たす汎用モーダルウィンドウコンポーネント ModalWindow を実装しました。

  • 任意のノードにアタッチするだけで「確認用ポップアップ」として機能。
  • 背景入力のブロック(画面全体のクリック/タッチ無効化)。
  • OK / Cancel ボタン押下時のコールバックをインスペクタから柔軟に設定可能。
  • 「本当に終了しますか?」などのメッセージを defaultMessageopen(message) で簡単に切り替え。
  • 外部の GameManager やシングルトンに一切依存しない、完全に独立した再利用可能コンポーネント。

UI の見た目(背景の色、パネルの画像、ボタンのスタイル)はプロジェクトごとに自由に変更できますが、ModalWindow のスクリプトはそのまま流用できます。
「確認ダイアログ」「設定変更の確認」「タイトルに戻る確認」など、さまざまな場面で同じコンポーネントを再利用することで、ゲーム全体の UI 実装をシンプルに保つことができます。

プロジェクトに一度組み込んでおけば、あとはノードにアタッチしてメッセージとコールバックを設定するだけで、どこでも同じ UX のモーダルを素早く追加できるようになります。

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