[JavaScript] 複数ゲームを切り替えるためのシンプルなシーン設計

はじめに

ゲームボックス内で、「ブロック崩し」と「シューティング」の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日間かけて、大晦日の夜から元旦の早朝にかけて学びました。