【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 には UITransform と Widget を使って画面全体を覆うように広げることを推奨します(後述の使用手順で説明)。
3. インスペクタで設定可能なプロパティ設計
backgroundNode: Node | null- モーダルの背景(半透明の黒など)のノード。
- ここに
ButtonやBlockInputEventsを付けてもよいですが、本コンポーネント内でイベントをブロックするため、必須ではありません。 - 未設定でも動作しますが、見た目上の暗転が必要な場合は設定してください。
panelNode: Node | null- メッセージやボタンを配置したパネルノード。
- 未設定でもエラーにはしませんが、通常は設定します。
messageLabel: Label | null- 確認メッセージを表示する
Labelコンポーネント。 - 未設定の場合、メッセージの自動変更は行われず、警告ログを出します。
- 確認メッセージを表示する
defaultMessage: string- モーダルを開いたときに表示するデフォルトメッセージ。
- 例: 「本当に終了しますか?」
okButton: Button | null- OK 決定用のボタン。
- 未設定の場合は OK ボタン機能が無効になり、警告ログを出します。
cancelButton: Button | null- キャンセル用のボタン(任意)。
- 未設定の場合は Cancel 機能は使われません。
closeOnBackground: booleantrueの場合、背景部分をクリック/タッチするとモーダルを閉じる。falseの場合、背景クリックでは閉じない(厳密な確認ダイアログなどに適用)。
autoOpenOnStart: booleantrueの場合、start()時に自動でモーダルを開く。- テストやチュートリアル用に便利。
blockInputOnModal: booleantrueの場合、モーダルが開いている間は画面全体の入力をブロックする。- 内部的には
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()autoOpenOnStartがtrueの場合、自動でopen()を呼び出し、defaultMessageを表示。
open(message?: string)- モーダルを開き、渡されたメッセージ(または
defaultMessage)をラベルに設定。 messageLabelが未設定なら警告ログ。
- モーダルを開き、渡されたメッセージ(または
close()- モーダル全体を非表示にし、
_isOpenをfalseに。
- モーダル全体を非表示にし、
- 入力ブロック
this.node.on(Node.EventType.TOUCH_START, this._touchBlocker, this, true)の第4引数trueにより、キャプチャフェーズでイベントを受け取ります。- モーダルが開いている間は
event.propagationStopped = true;として背景への伝播を止めることで、背景入力をブロック。
- コールバック呼び出し
onOkTarget+onOkMethodName/onCancelTarget+onCancelMethodNameを組み合わせて、任意のコンポーネントメソッドを呼び出す。- メソッドの存在チェックと try-catch による防御的実装。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択し、
ModalWindow.tsという名前で作成します。 - 自動生成されたコードをすべて削除し、本記事の
ModalWindowのコードを貼り付けて保存します。
2. モーダル用 UI ノードの作成
- Hierarchy パネルで右クリック → Create → UI → Canvas (既に Canvas がある場合は再利用)。
- Canvas の子として、右クリック → Create → UI → Empty Node を作成し、名前を
ModalRootに変更します。 ModalRootを選択し、Inspector の Add Component → UI → UITransform を追加します(通常は Canvas の子には既に付いていますが、なければ追加)。- 同じく
ModalRootに Add Component → UI → Widget を追加し、Left / Right / Top / Bottom をすべて 0 に設定してチェックを入れ、画面全体を覆うようにします。
3. 背景ノードの作成
ModalRootを右クリック → Create → UI → Sprite を作成し、Backgroundと命名します。Backgroundを選択し、Inspector の Sprite で- Color を
(0, 0, 0, 160〜200)程度の半透明黒に設定 - Size Mode を TRIMMED から CUSTOM に変更し、
UITransformの Width/Height を画面サイズに合わせる(または Widget コンポーネントで四辺 0 固定)。
- Color を
4. パネルとボタンの作成
ModalRootを右クリック → Create → UI → Sprite を作成し、Panelと命名します。Panelの Sprite で、ダイアログ風の背景画像や単色を設定し、UITransformで適度なサイズ(例: 400×250)に調整します。Panelを選択 → 右クリック → Create → UI → Label を作成し、MessageLabelと命名します。- テキストを仮に
本当に終了しますか?にしておきます。 - Alignment を中央揃えにし、パネル内で読みやすい位置に配置。
- テキストを仮に
Panelを選択 → 右クリック → Create → UI → Button を作成し、OkButtonと命名します。- Button の子に自動で Label が付きますので、テキストを
OKに変更。
- Button の子に自動で Label が付きますので、テキストを
- 同様に
Panelの子としてCancelButtonを作成し、テキストをキャンセルに変更します。 - OK / Cancel ボタンをパネル下部に左右に配置します。
5. ModalWindow コンポーネントのアタッチと設定
- Hierarchy で
ModalRootを選択します。 - Inspector の Add Component → Custom → ModalWindow を選択してアタッチします。
- Inspector 上の ModalWindow セクションで以下を設定します。
- Background Node:
Backgroundノードをドラッグ&ドロップ。 - Panel Node:
Panelノードをドラッグ&ドロップ。 - Message Label:
MessageLabelのLabelコンポーネントをドラッグ&ドロップ。 - Default Message:
本当に終了しますか?など任意の文言。 - Ok Button:
OkButtonのButtonコンポーネントをドラッグ&ドロップ。 - Cancel Button:
CancelButtonのButtonコンポーネントをドラッグ&ドロップ。 - 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 NameにonConfirmExitと入力。
- 例として、Canvas に
- On Cancel Target / On Cancel Method Name:
- キャンセル時に特別な処理が不要なら空のままで構いません。
- 何か処理したい場合は、上記 OK と同様に設定します。
- Background Node:
6. 動作確認
- 再生ボタン(Play)を押してゲームを実行します。
Auto Open On StartをONにしている場合、起動直後にモーダルが表示されます。- 背景が半透明で暗くなり、パネルとメッセージ、OK / Cancel ボタンが表示されていることを確認します。
- OK ボタンをクリックすると:
- コンソールに
[ModalWindow] OK clickedのログが出る。 - 設定していれば、
onOkTarget.onConfirmExit()が呼ばれる。 - モーダルが閉じて UI が消える。
- コンソールに
- Cancel ボタンをクリックすると:
- コンソールに
[ModalWindow] Cancel clickedのログが出る。 - 設定していれば、
onCancelTarget.onCancelSomething()が呼ばれる。 - モーダルが閉じる。
- コンソールに
- モーダルが開いている状態で、背景部分をクリック/タッチしてみます。
Block Input On Modalがtrueのため、背景のボタンなどは反応しないことを確認します。Close On Backgroundがtrueの場合は、背景クリックでモーダルが閉じることを確認します。
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 ボタン押下時のコールバックをインスペクタから柔軟に設定可能。
- 「本当に終了しますか?」などのメッセージを
defaultMessageやopen(message)で簡単に切り替え。 - 外部の GameManager やシングルトンに一切依存しない、完全に独立した再利用可能コンポーネント。
UI の見た目(背景の色、パネルの画像、ボタンのスタイル)はプロジェクトごとに自由に変更できますが、ModalWindow のスクリプトはそのまま流用できます。
「確認ダイアログ」「設定変更の確認」「タイトルに戻る確認」など、さまざまな場面で同じコンポーネントを再利用することで、ゲーム全体の UI 実装をシンプルに保つことができます。
プロジェクトに一度組み込んでおけば、あとはノードにアタッチしてメッセージとコールバックを設定するだけで、どこでも同じ UX のモーダルを素早く追加できるようになります。




