[Web App] LAYER_FIGHTER ─ R3Fで作ったVR対応 3D格闘ゲーム
はじめに
「LAYER_FIGHTER」は、Three.js / React Three Fiber で構築したブラウザベースの3D格闘ゲームです。
lain-lab.comに搭載された5つ目のゲームタイトルで、VRMアバターが対面して殴り合うフレーム単位の格闘ゲームエンジンを実装しています。
テンキー表記の方向入力、startup/active/recoveryのフレームデータ、ヒットボックス判定、ヒットストップ、ラウンド制KOシステムを備え、キーボード・PS4コントローラー・Meta Quest(WebXR)の3入力デバイスでプレイ可能です。
Youtube紹介動画
LAUNCH_COMMAND: ftg
serial experiments lain 世界観 × フレーム単位3D格闘
設計思想 ─ フレーム単位の格闘エンジン
他の4タイトルがリアルタイム物理やターン制だったのに対し、LAYER_FIGHTERは60FPS固定のフレーム駆動ゲームループで動作します。
useFrame 内でデルタタイムを蓄積し、1000/60 ms 単位で物理演算・入力処理・ヒット判定を実行する固定フレームレート方式です。
プレイヤーの状態は FighterState 型のステートマシンで管理され、各状態にJSON定義のフレームデータ(startup・active・recovery)が紐づきます。
ゲームモード構成
FTGManager.tsx がゲーム全体のモード遷移を管理します。
| モード | 内容 |
|---|---|
| TITLE | タイトル画面。メニュー選択 |
| STAGE_SELECT | GLBステージ3Dプレビュー付き選択画面 |
| TRAINING | トレーニングモード(対CPU戦) |
| SOUND_TEST | BGM/SE再生テスト |
| CREDIT_SEQUENCE | クレジットスライダー |
| SHUTDOWN | 終了 |
入力システム ─ テンキー表記
格闘ゲームの慣例に従い、方向入力をテンキー表記(1〜9)で管理しています。5がニュートラル、8が上、2が下、6が前方、4が後方。キャラクターの向き(facingRight)に応じて物理的な左右が forward/backward に自動変換されます。
入力デバイス統合
3種の入力デバイスを1つのゲームループ内で統合しています。
| デバイス | 方向 | ボタン |
|---|---|---|
| キーボード | WASD / 矢印キー | Z(P) X(K) C(G) V(E) |
| PS4コントローラー | Dpad / 左スティック | ×(P) ○(K) □(G) △(E) |
| VR(Quest) | 左スティック | 右A(P) 右B(K) 左グリップ(G) 左トリガー(E) |
PS4コントローラーは navigator.getGamepads() によるポーリング方式。VRコントローラーは XRSession.inputSources から毎フレーム読み取り。
PS4メニュー操作フック ─ useFTGMenuPad.ts
タイトル、ステージセレクト、サウンドテスト、確認ダイアログなど全メニュー画面で共通利用されるPS4パッド入力フック。Dpad/左スティックでメニュー移動、×ボタンで決定、○ボタンでキャンセル。エッジ検出(前フレームとの差分)で長押し連打を防止し、方向入力には250msのクールダウンを設けています。
入力バッファ
直近10フレームの方向入力を inputBufferRef に蓄積し、将来的なコマンド入力(波動拳コマンド等)の判定に使用可能な基盤を構築しています。
戦闘エンジン ─ useFTGGameLoop.ts
FighterState ステートマシン
プレイヤーの状態は以下の FighterState 型で管理されます。
| 状態 | アニメーション | 説明 |
|---|---|---|
| IDLE | IDLE_CHANGE_POSE | 立ち待機 |
| WALKING_F | SYS_PLAYER_WALKING_F | 前進 |
| WALKING_B | SYS_PLAYER_WALKING_B | 後退 |
| CROUCHING | SYS_CROUCHING | しゃがみ |
| JUMPING | SYS_RUNNING | ジャンプ(空中) |
| PUNCHING | SYS_PUNCH | 立ちパンチ |
| CROUCH_PUNCH | SYS_CROUCH_PUNCH | しゃがみパンチ |
| KICKING | SYS_KICK | 立ちキック |
| KICK_KNEE | SYS_KICK_KNEE | 膝蹴り |
| KICK_FORWARD | SYS_KICK_FORWARD | 前蹴り |
| KICK_LOW | SYS_KICK_LOW | 下段蹴り |
| GUARDING | SYS_GUARD | 立ちガード |
| CROUCH_GUARDING | SYS_CROUCH_GUARD | しゃがみガード |
| HITSTUN | SYS_HURT | 被弾硬直 |
| JUMP_PUNCH | SYS_PUNCH | 空中パンチ |
| JUMP_KICK | SYS_KICK | 空中キック |
技フレームデータ ─ Moves.json
各攻撃技は Moves.json にフレームデータとして定義されています。
| パラメータ | 説明 |
|---|---|
| state | 対応する FighterState |
| startup | 発生フレーム(判定が出るまでの予備動作) |
| active | 持続フレーム(攻撃判定が有効な期間) |
| recovery | 硬直フレーム(技の後隙) |
| damage | ダメージ値 |
| knockback | ノックバック距離 |
| hitboxes | 攻撃判定の座標・サイズ配列 |
| hitSe / hitSeVolume | ヒット時のSEとボリューム |
| animSpeed | アニメーション再生速度倍率 |
技の総持続時間は startup + active + recovery で算出され、この期間中はステートが固定されて他の入力を受け付けません。
ヒットボックス判定
プレイヤーと敵のそれぞれに「食らい判定(ハートボックス)」と「攻撃判定(ヒットボックス)」が独立して設定されています。
食らい判定は常時表示の BoxGeometry(0.5×1.6×0.4)。攻撃判定は Moves.json の hitboxes 配列で複数箇所を定義可能で、startup中は0番目のみ縮小表示、activeフレームに入ると全ヒットボックスがフルサイズで判定開始。
判定はXY軸のAABB(軸並行バウンディングボックス)交差テスト。キャラクターの向きに応じてX座標のオフセットが反転します。
ヒットストップ
攻撃命中時、hitStopRef に指定フレーム数がセットされ、その間すべてのゲームロジック(物理演算・入力処理・AI)が完全に停止します。これにより格闘ゲーム特有の「ヒット時の衝撃感」を演出しています。
物理演算
ジャンプは初速 JUMP_VY = 0.18、重力 GRAVITY = 0.012/frame の放物運動。着地判定は posY <= 0 で即座に接地。キャラクター間の押し返しは最小距離(MIN_DIST = 1.0)を保つように双方を均等に移動させます。ステージ端の移動制限は STAGE_LIMIT = 5.5。
ラウンドシステム
2ラウンド先取制(KOで1本)。ラウンドフェーズは FIGHTING → KO → WAIT_INPUT → ROUND_CALL → FIGHT_CALL → FIGHTING のサイクルで管理されます。
| フェーズ | 表示 | 持続 |
|---|---|---|
| KO | 「K.O.」大文字赤表示 | 約1秒 |
| WAIT_INPUT | 「YOU WIN / YOU LOSE」+スコア | ボタン待ち |
| ROUND_CALL | 「ROUND N」シアン表示 | 約1.5秒 |
| FIGHT_CALL | 「FIGHT!」赤表示 | 約1秒 |
| FIGHTING | バトル進行中 | HP0まで |
2本先取でGAME_SETとなり、VICTORY表示後にタイトルに戻ります。
敵AI
敵は EnemyState(IDLE / WALKING / COOLDOWN / ATTACKING / HITSTUN)の5状態で動作する簡易AIで、プレイヤーとの距離に応じて接近→攻撃→クールダウンのサイクルを繰り返します。攻撃技は Assets.json の敵VRM定義に紐づいた技リストからランダム選択。歩行速度やAIパラメータもJSON定義されています。
ステージセレクト ─ FTGStageSelect.tsx
5つのバトルステージがGLBモデルとして用意されています。
| ステージ | 制作者 | ライセンス |
|---|---|---|
| Sci-fi Fighting | Umar | CC Attribution |
| Lava Fighting Arena | Umar | CC Attribution |
| Mitaiken Horizon | tspmosybau | CC Attribution |
| Hikari LLSIFAS | tspmosybau | CC Attribution |
| Yume Utaou LLSIFAS | tspmosybau | CC Attribution |
ステージ選択画面では左側にステージ名リスト、右側にGLBモデルのリアルタイム3Dプレビュー(ゆっくり回転)が表示されます。
VRMキャラクター ─ FTGComponents.tsx
FTGCharacter
VRMモデルのロード・アニメーション再生・物理座標追従を担うコンポーネント。physicsRef の座標を毎フレーム group.position に反映し、向き(facingRight)に応じて -π/2 または +π/2 に回転。
アニメーション制御
ANIM_MAP が FighterState からアニメーションIDへの変換テーブル。攻撃アニメは LoopOnce + clampWhenFinished で1回再生後に最終フレームで停止。しゃがみ・ガードも LoopOnce + clamp で姿勢を保持。アニメーション速度は Moves.json の animSpeed または Assets.json の animations[].animSpeed で技ごとに個別調整可能。
カメラ ─ FTGCamera
プレイヤーと敵の中間点をフォーカスし、距離に応じてズームアウトする横視点カメラ。FOV 50で固定。
HUD ─ FTGBattleHUD.tsx
HTML DOMオーバーレイで実装されたバトルHUD。ftgHudRef(Reactのレンダリングサイクルと分離したRefベースのストア)から30FPSでポーリングして表示更新。
トレーニングモード表示
トレーニングモードでは追加のデバッグ情報が表示されます。
| 表示 | 内容 |
|---|---|
| テンキーグリッド | 現在の方向入力を9マスで可視化 |
| ボタン表示 | P/K/G/Eの押下状態をカラー表示 |
| フレームカウンター | 5桁のフレーム番号 |
| 入力ログ | 直近6件のステート遷移ログ |
サウンドテスト ─ FTGSoundTest.tsx
Assets.json に定義された全BGM・SEを一覧表示し、個別に再生テストできるモード。上下キーで選択、Enterで再生。BGMとSEが分離されたタブ形式で表示されます。
クレジット ─ FTGCreditSequence.tsx
Assets.json と Stages.json からVRMモデル制作者・ステージ制作者・BGM作曲者・SE制作者を自動抽出し、スライダー形式で1エントリずつ表示するクレジットシーケンス。
各エントリは INCOMING(右から滑り込み)→ STAY(静止表示)→ OUTGOING(左へスライドアウト)の3フェーズで遷移。3Dモデルタイプのエントリは自動回転プレビュー付き、画像タイプはテクスチャビルボード表示。
コードアーキテクチャ
src/components/PROJECT_LAIN/FTG/
├── FTGManager.tsx 356行(モード遷移・BGM切替・メニュー制御)
├── FTGComponents.tsx 866行(VRMキャラ・カメラ・ステージ・HP・ヒットボックス)
├── useFTGGameLoop.ts 836行(ゲームループ・入力・物理・ヒット判定・ラウンド制)
├── FTGTrainingScene.tsx 543行(トレーニングモードシーン)
├── FTGTitleBackground.tsx 639行(タイトル画面3D背景)
├── FTGStageSelect.tsx 288行(ステージセレクト+GLBプレビュー)
├── FTGSoundTest.tsx 620行(サウンドテストモード)
├── FTGCreditSequence.tsx 426行(クレジットスライダー)
├── FTGBattleHUD.tsx 109行(バトルHUDオーバーレイ)
├── FTGHudStore.ts 34行(Refベースの状態ストア)
├── useFTGMenuPad.ts 79行(PS4メニュー操作共通フック)
├── utils/
│ └── AudioController.ts (BGM/SE管理)
└── data/
├── Assets.json (VRM・BGM・SE・アニメーション定義)
├── Moves.json (技フレームデータ)
└── Stages.json (バトルステージ定義)
技術的な振り返り
固定フレームレートの実装
useFrame のデルタタイムは可変なので、格闘ゲームに必要な固定60FPSロジックを実現するために accRef にデルタタイムを蓄積し、16.67ms単位でwhileループを回す方式を採用しています。これにより、ディスプレイのリフレッシュレートに関係なくフレームデータが正確に機能します。
ヒットボックスのプール方式
敵のヒットボックスは最大4個の THREE.Mesh を事前にマウントしておき、useFrame 内で visible と position/scale を直接操作するオブジェクトプール方式。Reactの再レンダリングを完全に回避しています。
HudStoreの分離
バトル中のHP・ラウンド情報・入力ログなどの高頻度更新データは ftgHudRef(プレーンなオブジェクトRef)に書き込み、HTMLオーバーレイ側は setInterval(1000/30) で30FPSのポーリング読み取りを行う設計です。ゲームループ(60FPS)とUI描画(30FPS)のフレームレートを分離することで、DOM更新のオーバーヘッドを抑えています。
VRセッションのグローバル共有
VR入力を useFTGGameLoop 内で読み取るために、XRSession を (window as any).__ftgXrSession にグローバル保存してフレームごとにアクセスする方式を採用しています。R3Fの useXR フックの外側(純粋なゲームループ内)からXR入力を読み取る必要があったためです。
LAUNCH_COMMAND: ftg
プロトコルに接続せよ。