[JavaScript] GameBoxアーキテクチャで作る Three.js ブロック崩し(実装完了編)

はじめに

昨日の記事の続きで、ブロック崩しゲームをThree.jsの箱庭空間内に実装したので、記録メモです。

手前に操作パネルを作ったので、VRでも操作できるようにしたほか、パソコンではキーボードで操作できるようにしています。

STARTボタンでゲーム開始し、下にボールが落ちるとゲームオーバー。

今回もVR動画を撮ってみました。

今日は疲れたので、簡易編集で…。

明日また記事を編集するかもしれません。

実装内容

今回はその続きとして、 同じ GameBox 上で ブロック崩しを最後まで実装する。

  • ボール移動
  • パドル操作
  • 壁・ブロック衝突
  • 勝利判定
  • ゲームオーバー表示
  • UIボタン連携(WebXR想定)

「ゲームを作る」のではなく、 ゲームを差し替えられる構造の中で完成させるのがテーマ。


GameModule という共通インターフェース

GameBox では、ゲーム本体を GameModule として扱う。

class GameModule {
  init(box) {}
  update(delta) {}
  dispose() {}
}
  • init : GameBox を受け取り、オブジェクトを配置
  • update : 毎フレーム処理
  • dispose : ゲーム切り替え時の後始末

このインターフェースを守れば、

  • ブロック崩し
  • インベーダー
  • パズル

を 同じ箱に差し替え可能になる。


BreakoutGame の全体像

class BreakoutGame extends GameModule {
  constructor() {
    super();
    this.ball = null;
    this.paddle = null;
    this.blocks = [];
    this.velocity = new THREE.Vector2();
    this.box = null;

    this.started = false;
    this.isAnimation = false;
    this.isVictory = false;
    this.isGameOver = false;
  }
}

重要なのは、

  • GameBox 自体は触らない
  • ゲームの状態はすべて BreakoutGame が持つ

という分離。


初期化:GameBox を舞台に使う

init(box) {
  this.box = box;
  this.walls = box.wall.walls;

  this.createBoll(box);
  this.createPaddle(box);
  this.createBlocks(box);

  this.isAnimation = false;
}
  • box.group が「ゲームの舞台」
  • 壁は GameWall から流用
  • 論理サイズは box.size を基準にする

座標をハードコードしないのがポイント。


ボール移動と delta 管理

update(delta) {
  this.updateButton();

  if (!this.isAnimation) return;
  if (!this.deltaTimeCheck(delta)) return;

  delta = Math.min(delta, config.gameBox.maxDelta);

  this.updateBoll(delta);
  this.updatePaddle(delta);
}
  • 初回フレームの delta 暴走対策
  • 33ms 制限で瞬間移動防止
  • UI入力と物理処理を分離

WebXR でも安定する構成。


当たり判定は Box3 ベース

this.ballBox = new THREE.Box3().setFromObject(this.ball);

Three.js では、

  • レイキャストより Box3 の方が軽い
  • 球体でも AABB で十分

ブロック・壁・パドルすべて 同じ方法で判定できる。


ブロックとの衝突と消去

if (this.ballBox.intersectsBox(blockBox)) {
  this.box.group.remove(block);
  this.blocks.splice(i, 1);
  this.velocity.y *= -1;
  this.checkVictory();
  break;
}
  • 1フレーム1ヒット
  • 配列後ろからループ
  • 消した瞬間に勝利判定

余計な演出を入れず、まず完成させる。


勝利判定

checkVictory() {
  if (this.blocks.length === 0) {
    this.isVictory = true;
    this.isAnimation = false;
  }
}

「止める」だけが重要。

  • 演出は後から足せる
  • 物理を止める方が先

ゲームオーバー判定(下に落ちたら終了)

const halfBoxH = this.box.size.y / 2 - 0.3;
const ballRadius = (this.ballBox.max.y - this.ballBox.min.y) / 2;

if (this.ball.position.y < -halfBoxH - ballRadius) {
  this.gameOver();
  return;
}
  • 下壁は「反射」ではなく「敗北」
  • 壁判定とは分ける
  • ルール判定は updateBoll の最後に集約

GAME OVER 表示(Sprite + Canvas)

const sprite = this.createTextSprite('GAME OVER');
sprite.position.copy(this.box.group.position);
sprite.position.z += 0.5;
config.scene.add(sprite);

なぜ Sprite か

  • 常にカメラ正面
  • lookAt 不要
  • WebXR / 非XR 共通で使える

UI を 3D 世界に自然に溶け込ませられる。


CanvasTexture で文字を描く

ctx.font = '80px sans-serif';
ctx.fillText(text, canvas.width / 2, startY);
  • DOM を使わない
  • XR 対応が楽
  • フォント・装飾を自由に制御できる

Three.js で UI を作るなら定番構成。


UIボタンとの連携

UI ボタンは キー入力をエミュレートする。

button.userData.onClick = () => {
  config.keyState['Enter'] = true;
  setTimeout(() => {
    config.keyState['Enter'] = false;
  }, 100);
};

これにより、

  • キーボード
  • マウス
  • VRコントローラ

すべて同じゲームロジックで動く。

入力層とゲーム層を分離できる。


GameBox 構造の強み

この構成で得られたもの:

  • ゲームを丸ごと差し替え可能
  • UI / 入力 / 物理が混ざらない
  • WebXR に自然に拡張できる
  • Three.js の「表示されない問題」を避けやすい

「一つのゲームを作る」より、 「ゲームが載る箱を作る」方が後が実装が楽。


次回

今回作成したゲームボックスを利用して、ゲーム選択画面の作成、更に、 物理演算的なアートアニメ―ションか、簡易的なシューティングゲームを作成する予定です。