はじめに
ゲームボックス内で、「ブロック崩し」と「シューティング」の2つを作成しましたが、今回は、この2つを切り替えて表示できる実装をしたので、その備忘録メモです。
操作パネルの左右にモニタオブジェクトを設置して、左のパネルには、ゲームのサムネ画像を表示して、レーザーポインタで選択するとゲームが表示されるという内容です。
右のモニタには、簡易的なゲームの説明、プレイ方法を表示する用にしてます。
この内容に関しては、過去の実装内容の応用なので割愛。
今回は、2つのゲームを切り替えるクラス設計ロジックについて簡単に紹介します。
今回作成したファイルは以下の4つ
./js/scene/GameBase.js
./js/scene/SceneBase.js
./js/scene/SceneManager.js
./js/scene/TitleScene.js
今回は、ChatGPTが全く役に立たず、コードがスパゲッティ化しそうになり、自力で全ロジックを考えたり、ゲーム内のコードも修正しましたが、それでも分からなかったところはAIに部分的に聞いて修正しています。
大晦日から、元旦にかけて、2日がかりで組んだロジックなのでかなり疲れました。
いつも通り、完成した動作確認のVR動画もアップしておきます。
全体構成の考え方
今回の設計では、「ゲーム本体」と「ゲームを切り替える仕組み」を明確に分離しています。
ポイントは以下の3点です。
- 各ゲーム(ブロック崩し/シューティング)は単体で完結させる
- ゲーム切り替えは SceneManager + Scene に責務を集約する
- 「状態監視」と「実行処理」を混ぜない
これにより、
- ゲームロジック側は切り替えを一切意識しない
- シーン切り替えの挙動がログで追いやすい
- ファイル数を増やさず、流れを直線的に保てる
という構造になります。
SceneBase.js(シーンの共通インターフェース)
export class SceneBase {
enter(gameBox) {}
update(dt) {}
exit() {}
}
すべてのシーンはこの3つのライフサイクルだけを持ちます。
enter: シーン開始時の初期化update: 毎フレーム呼ばれる処理exit: シーン終了時の後始末
抽象化は最低限に留め、 「どのシーンも同じ形で扱える」ことだけを保証します。
GameBase.js(ゲームをシーンとして包む)
export class GameBase extends SceneBase {
constructor(gameInstance) {
super();
this.game = gameInstance;
}
enter(gameBox) {
this.game.init(gameBox);
}
update(dt) {
if (this.game) {
this.game.update(dt);
}
}
exit() {
if (this.game && this.game.dispose) {
this.game.dispose();
}
}
}
GameBase は 「ゲームをシーンとして扱うための薄いラッパー」です。
- ゲーム本体は
init / update / disposeだけを実装 - SceneManager からは 常に Scene として扱える
この構造にすることで、
- ゲームの切り替え
- タイトル画面 → ゲーム画面
- ゲーム → 別ゲーム
をすべて同じ change(scene) で処理できます。
SceneManager.js(シーンの管理と切り替え)
export class SceneManager {
constructor(gameBox) {
this.gameBox = gameBox;
this.current = null;
}
change(scene) {
if (this.current) this.current.exit();
this.current = scene;
this.current.manager = this;
if (this.gameBox) {
this.current.enter(this.gameBox);
}
}
update(delta) {
if (this.current) {
this.current.update(delta);
}
}
}
SceneManager の責務は非常にシンプルです。
- 現在のシーンを1つだけ保持
- 切り替え時に
exit → enterを保証 - 毎フレーム
updateを委譲
ここにゲームロジックや入力処理は置きません。
TitleScene.js(ゲーム選択と切り替え)
TitleScene は今回の設計の要になります。
役割
- 背景用ゲーム(ballBound)の再生
- 3Dモニタからの入力監視
- 選択されたゲームを SceneManager に渡す
切り替え処理の核心部分
if (config.gameState.selectedGame) {
const selectedKey = config.gameState.selectedGame;
let targetGameCls = null;
if (selectedKey === 'breakout') targetGameCls = BreakoutGame;
if (selectedKey === 'shootInvaders') targetGameCls = ShootingGame;
if (targetGameCls) {
// フラグを即リセット(重要)
config.gameState.selectedGame = null;
// シーン切り替え
this.manager.change(new GameBase(new targetGameCls()));
return;
}
}
設計上のポイント
selectedGameは イベント扱い(1フレームで消す)- フラグを残さないことで無限ループを防止
returnを入れて同一フレーム内の多重処理を防止
ここを明確にしたことで、
- 「どこで切り替わるか」
- 「なぜ切り替わったか」
をログだけで追えるようになります。
今回の設計で意識したこと
- 問題が出たら ファイルを増やさない
- 責務が曖昧になったら 削る方向で考える
- 状態は「保持」ではなく「瞬間イベント」として扱う
- 1人で読んで理解できる構造を優先する
結果として、
- シーン構造は4ファイルだけ
- ゲーム本体は完全に独立
- 切り替えロジックは TitleScene + SceneManager に集約
という、非常に見通しの良い構成になりました。
おわりに
今回の実装は、
- ゲーム数が少ない
- 切り替え条件が単純
という前提で、最小構成を意識した設計になっています。
ゲーム数が増えた場合でも、
- GameBase
- SceneManager
- TitleScene
の構造自体はそのまま流用可能です。
「まずシンプルに動かす」ための一例として、 同様の構成を考えている方の参考になれば幸いです。
あとがき
具体的には、各ゲームの実装方法の見直しや、設計のし直しなど、もう少し複雑なのですが、今回は割愛します。
ミニゲームを切り替えるだけなのですが、設計が如何に大事かというのを2日間かけて、大晦日の夜から元旦の早朝にかけて学びました。
💬 コメント