はじめに
昨日の記事の続きで、ブロック崩しゲームをThree.jsの箱庭空間内に実装したので、記録メモです。
[JavaScript] Three.js / WebXRで差し替え可能なゲーム箱を作る(3D空間でブロック崩しゲーム)
Three.js と WebXR を使って、VR空間に配置できる差し替え可能なゲーム用ボックスを実装。ブロック崩しを例に、THREE.Group を基軸にした設計、ゲームモジュールの分離、2Dロジックを3D空間で動かす考え方を整理します。
https://humanxai.info/posts/javascript-threejs-webxr-gamebox-architecture/手前に操作パネルを作ったので、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 の「表示されない問題」を避けやすい
「一つのゲームを作る」より、 「ゲームが載る箱を作る」方が後が実装が楽。
次回
今回作成したゲームボックスを利用して、ゲーム選択画面の作成、更に、 物理演算的なアートアニメ―ションか、簡易的なシューティングゲームを作成する予定です。
💬 コメント