[Web App] LAYER_ADV_PROTOCOL ─ R3Fで作ったVR対応 3D ADV/RPG
はじめに
「LAYER_ADV_PROTOCOL」は、Three.js / React Three Fiber で構築したブラウザベースの3D ADV/RPGです。
lain-lab.comに搭載された4つのゲーム(STG Protocol、Witchadius、LAYER_PUZZLE)の中で最大規模のプロジェクトで、自由探索・NPC会話・ターン制バトル・レベルアップ成長・ストーリー分岐・セーブ/ロードを備えたフルスケールのRPGシステムを実装しています。
serial experiments lainの世界観を「電脳空間の異層(LAYER)を探索し、感情を取り戻す」物語として構成し、PCブラウザとMeta Quest(WebXR)の両方でプレイ可能です。
Youtube紹介動画
LAUNCH_COMMAND: adv
serial experiments lain 世界観 × 3D自由探索 × ターン制RPG
設計思想 ─ 異層探索RPG
他の3タイトル(STG、Witchadius、PUZZLE)がアクション/パズルの瞬発力型だったのに対し、ADV Protocolは「空間を歩き、人と話し、敵と戦い、物語を進める」時間経過型の体験を目指しています。
ゲーム状態は EXPLORE(探索)/ BATTLE(バトル)/ ENDING(エンディング)の3フェーズで管理され、探索中にエンカウント条件を満たすとフェードアウト→バトルステージへ遷移、勝利すると元の座標に復帰する構造です。
すべてのステージ・NPC・敵・アイテム・成長テーブル・魔法・効果音はJSONファイルで外部定義されており、コードを触らずにゲームバランスの調整やステージ追加が可能です。
ストーリー進行 ─ storyStep駆動
ストーリーは storyStep(0〜4)の整数値で管理されます。
特定のステージに入る、ボスを倒すなどの条件でstepが進行し、NPC会話テキスト・ワープポイントの表示/非表示・ボスNPCの出現がステップに連動して切り替わります。
| step | トリガー | 変化 |
|---|---|---|
| 0→1 | 大聖堂(STAGE_CATHEDRAL)に初入場 | 森にボス「HACKLOAD」が実体化 |
| 1→2 | HACKLOAD撃破 | 街のNPC会話が変化(感情が戻る) |
| 2→3 | 大聖堂に再入場 | 地下洞窟(STAGE_CAVE)へのワープゲートが開通 |
| 3→4 | ラスボス「AKUMA」撃破 | ボス撃破ダイアログ→エンディングシーケンス起動 |
NPC会話は STORY_NPC_DIALOGUES マップで npcId × storyStep の組み合わせごとに個別テキストを定義しています。
探索モード ─ EXPLORE
GLBステージ
5つの探索ステージがGLBモデルとして用意され、Stages.json に定義されています。
| ステージ | 名前 | スケール | 制作者 |
|---|---|---|---|
| STAGE_MY_ROOM | Ireina’s room | 1.0 | Katydid. |
| STAGE_CITY | City | 12.0 | Nioma van der Steen |
| STAGE_CATHEDRAL | Cathedral | 1.0 | Yu Kai Him Otto |
| STAGE_FOREST | Forest | 7.0 | Katydid. |
| STAGE_CAVE | Underground cave | 1.0 | Katydid. |
各ステージにはBGM、スポーン座標、カメラ半径、フォグ設定、NPC配置、ワープポイント、セーブポイント、回復ポイント、エンカウント設定が個別に定義されています。
FPS移動 ─ ADVController.tsx
探索中のプレイヤー操作はポインターロック+WASD移動のFPS方式。three-mesh-bvh によるBVH(Bounding Volume Hierarchy)加速レイキャストで地面追従と壁当たり判定を実装しています。
| パラメータ | 値 | 説明 |
|---|---|---|
| MOVE_SPEED | 4.0 | 通常移動速度 |
| BROOM_SPEED | 8.5 | 箒飛行時の移動速度 |
| PLAYER_HEIGHT | 1.4 | プレイヤーの身長(レイキャスト基準) |
| MAX_STEP_HEIGHT | 0.6 | 段差の最大乗り越え高さ |
| WALL_OFFSET | 0.4 | 壁との最小距離 |
地面追従は下方向レイキャスト、壁判定は前方レイキャストで処理。地面の高さ変化は GROUND_LERP_SPEED: 0.15 の補間で滑らかに追従し、段差のデッドゾーン(0.05)以下の微小変化は無視してチラつきを防いでいます。
スペースキーでジャンプ(初速5.2、重力加速度-9.8)が可能で、接地判定は下方レイキャストの距離で判断します。
ワープポイントとNPC
ワープポイントは WavyRing(波紋リング+パーティクル)で視覚化され、プレイヤーが近づくとフェードアウト→ステージ遷移→フェードイン。NPCは各ステージの npcs 配列で定義されたVRMモデルが固定位置に立ち、近づくと会話テキストが表示されます。
セーブ/ロード ─ SaveMenu
セーブポイント(紫色のWavyRing)に近づくと SaveMenu が開き、IndexedDBにプレイヤーレベル・経験値・HP/MP・ストーリー進行度・現在ステージ・座標・向きを永続保存。ロード時はすべての状態を復元してプレイを再開できます。
回復ポイント
自室のベッド付近に設置された回復ポイント(エメラルド色のWavyRing)に近づくと回復メニューが開き、「YES」を選ぶとフェードアウト→BGM停止→5秒間の暗転(睡眠SE再生)→HP/MP全回復→フェードインの演出が再生されます。
箒飛行モード
レベルアップで MAGIC_BROOM スキルを習得すると、Bキー(VRではグリップ)で箒飛行モードを切り替え可能。アバターが前傾姿勢(-45度、TILT_LERP_FACTOR: 0.15 で滑らかに補間)になり、頭(Headボーン)は逆回転してカメラ正面を向き続けます。移動速度は通常の約2倍(8.5 vs 4.0)。WiredAccessory コンポーネントが箒GLBモデルをVRMの手ボーンにアタッチします。
バトルシステム ─ ADVBattle.tsx
ターン制コマンドバトル
バトルフェーズは PLAYER_TURN → PLAYER_ANIM → ENEMY_TURN → (ループ or BATTLE_END)の4状態。
プレイヤーターンでは3つのメインコマンドから選択します。
| コマンド | 動作 |
|---|---|
| ATTACK(たたかう) | 通常攻撃。魔法弾がプレイヤーから敵へ飛び、着弾でダメージ |
| MAGIC(まほう) | サブメニューに遷移。習得済み魔法の一覧から選択 |
| ESCAPE(にげる) | 戦闘から離脱。元の探索座標に復帰 |
魔法システム ─ Magic.json
魔法は Magic.json で定義され、レベルアップ時に PlayerGrowth.json の skills 配列に追加されます。
| 魔法 | 効果タイプ | MP消費 | 概要 |
|---|---|---|---|
| MAGIC_SHOT | ATTACK_ONLY | 5 | マジックショット(通常魔法弾) |
| MAGIC_HEAL | HEAL | 10 | HP30回復 |
| MAGIC_LIGHT | PASSIVE | 0 | 暗いステージで周囲を照らすライト |
| MAGIC_BROOM | TOGGLE_MODE | 0 | 箒飛行モードの切り替え |
| MAGIC_METEOR | ATTACK_ONLY | 20 | メテオストライク(高火力魔法) |
| MAGIC_STARDUST | ATTACK_ONLY | 15 | 星屑の系譜(5発の追尾星弾) |
| MAGIC_HEAL_PLUS | HEAL | 20 | HP60回復 |
攻撃魔法はそれぞれ独立したエフェクトシステム(ADVChargeSystem、ADVMagicBurstSystem、ADVStardustTrail)で演出されます。
魔法弾エフェクト ─ MagicProjectile.tsx
通常攻撃の魔法弾は MagicProjectile コンポーネントが始点から終点へ線形補間で飛行する球体メッシュ。速度2.2(約0.45秒で到達)、AdditiveBlending で発光。着弾時に onHit コールバックでダメージ処理が発火します。
星屑の系譜 ─ ADVStardustTrail.ts
5発の星型弾(ExtrudeGeometry の五芒星)がベジエ曲線を描いてランダムな放物線を飛び、敵に収束着弾。各弾には0.38秒の連射ディレイ、ランダムな制御点による曲がり、自転アニメーション。軌跡にはカラフルな星屑パーティクル(4色のシアン・黄・緑・紫)がドロップされます。
発射と同時に足元に4重回転魔法陣が展開。全弾着弾後に自動クリーンアップされます。
敵の暗黒魔法 ─ EnemyDarkMagic.ts
ラスボスの特殊攻撃。2フェーズ構成で、CASTING(1.0秒の詠唱チャージ球体)→ SOUL_CHASE(4体の人魂がプレイヤー胸元を高速公転しながら収束→着弾)。人魂はワイヤーフレームの二重球体で、軌跡にディープブルーの火の粉パーティクルが立ち上ります。
敵の特殊攻撃レジストリ
敵ごとの特殊攻撃は specialAttackRegistry マップでシステムクラスにルーティングされ、JSONで定義された "type" 文字列とエフェクトシステムが完全に分離されています。
| type | システムクラス | 演出 |
|---|---|---|
| APPLE_DROP | EnemyAppleDrop | リンゴが上空から降り注ぐ |
| REFLECTION_ARROW | EnemyReflectionArrow | 7本の矢が反射しながら飛来 |
| DARK_MAGIC | EnemyDarkMagic | 人魂4体が高速公転→収束着弾 |
バトルUI ─ BattleUI.tsx
3D空間内に配置されたHP/MPゲージ+コマンドメニュー。ゲージは PlaneGeometry の幅をHP/MP比率でリアルタイムに変化させ、コマンド数に応じてパネルの高さと間隔が動的に算出されます。アニメーション中はコマンド部分のみ非表示にする showCommands フラグで制御。
レベルアップ成長 ─ PlayerGrowth.json
レベルアップテーブルは PlayerGrowth.json で完全外部定義。各レベルに最大HP・最大MP・習得スキル・次レベルまでの必要経験値が設定されています。バトル勝利時に経験値を獲得し、閾値に達するとレベルアップ→新スキル解放。
ステータスメニュー(Tabキー / VR Aボタン)で現在のレベル・HP/MP・経験値・習得スキル一覧を確認でき、回復魔法をフィールド上で使用可能です。
VRMアバター ─ ADVAvatar.tsx
探索時・バトル時の両方で使用されるVRMアバターコンポーネント。@pixiv/three-vrm でVRMモデルをロードし、@pixiv/three-vrm-animation で .vrma アニメーションファイルを createVRMAnimationClip() で変換して AnimationMixer で再生します。
アニメーション制御 ─ useADVAnimation.ts
外部から activeAnimId を指定することで任意のアニメーションを即時再生可能。指定がなければ isMoving に基づいて自動的にアイドル/走りを切り替えます。アニメーション間の遷移は fadeIn/fadeOut(0.2秒)でスムーズに補間。
| 状態 | アニメーションID |
|---|---|
| 待機 | IDLE_CHANGE_POSE |
| 走り | SYS_RUNNING |
| ジャンプ | ADV_PLAYER_JUMP |
| 箒待機 | SYS_BROOM_STILL |
| バトル詠唱 | ADV_MAGIC_SHOT |
| バトル被弾 | ADV_PLAYER_DAMAGE |
アクセサリシステム
箒モード時は WiredAccessory コンポーネントがアクセサリJSON(accessories.json)からホワイトリスト(ADV_ACCESSORY_WHITELIST = ['Broom_01'])に一致するアイテムのGLBモデルをVRMの手ボーンにアタッチします。
エンディング ─ ADVEndingScene.tsx
ラスボス撃破後のダイアログ完了で isEndingActive = true がセットされ、4フェーズのエンディングシーケンスが起動します。
| フェーズ | 内容 | 持続時間 |
|---|---|---|
| CHARACTER_INTRO | 全VRMキャラクターを1体ずつ回転表示+制作者名 | 各10秒 |
| STAGE_INTRO | 全GLBステージをミニチュアで回転表示+制作者名 | 各10秒 |
| STAFF_ROLL | 半透明スクリーン上をクレジットテキストがスクロール | 自動 |
| THANK_YOU | 「Thank you for playing. / See you in the Wired.」フェードイン→保持→フェードアウト | 6.5秒 |
エンディング開始時に既存BGMを停止し、エンディング専用BGM(Spark Of Inspiration / Denis Pavlov)に切り替え。全フェーズ完了後に window.location.reload() でページリロードしてタイトルに戻ります。
アセットキャッシュ ─ IndexedDB
全アセット(VRMモデル、GLBステージ、BGM、SE、画像、アニメーション)はDexie.jsベースのIndexedDBにバージョン管理付きでキャッシュされます。初回ロード時にネットワークから取得→IndexedDBに保存、2回目以降はIndexedDBから即座にBlob URLを生成してロード。
アセット解決は起動時に4種の非同期 useEffect(VRM、Audio、Stage、Image)が並列実行され、すべての Map<string, string>(元パス→Blob URL)が揃うまでUIは描画されません。
コードアーキテクチャ
src/components/PROJECT_LAIN/ADV/
├── ADVManager.tsx 1806行(ゲームループ本体・状態管理・ストーリー制御)
├── ADVBattle.tsx 1127行(ターン制バトルシステム)
├── ADVController.tsx 486行(FPS探索移動・BVHレイキャスト)
├── ADVAvatar.tsx 185行(VRMアバター・箒モード・アクセサリ)
├── ADVStage.tsx 55行(GLBステージ描画・チラつき対策)
├── ADVEndingScene.tsx 519行(4フェーズエンディングシーケンス)
├── BattleUI.tsx 135行(バトルHUD・コマンドメニュー)
├── MagicProjectile.tsx 45行(魔法弾飛翔コンポーネント)
├── ADVCreditBoard.tsx 65行(インゲームクレジット看板)
├── hooks/
│ └── useADVAnimation.ts 70行(アニメーション制御フック)
├── utils/
│ ├── ADVChargeSystem.ts (チャージ魔法陣エフェクト)
│ ├── ADVHealSystem.ts (ヒール魔法エフェクト)
│ ├── ADVMagicBurstSystem.ts (メテオ魔法エフェクト)
│ ├── ADVStardustTrail.ts 280行(星屑の系譜エフェクト)
│ ├── EnemyAppleDrop.ts (リンゴ雨エフェクト)
│ ├── EnemyReflectionArrow.ts (反射矢エフェクト)
│ ├── EnemyDarkMagic.ts 205行(暗黒魔法・人魂収束エフェクト)
│ └── AudioController.ts (BGM/SE管理シングルトン)
└── data/
├── Stages.json (ステージ定義)
├── Avatars.json (キャラクター定義)
├── BattleConfigs.json (バトルステージ定義)
├── Magic.json (魔法テーブル)
├── PlayerGrowth.json (レベルアップ成長テーブル)
├── SoundEffects.json (SE定義)
├── Images.json (画像アセット定義)
└── StoryTexts.ts (NPC会話テキスト)
技術的な振り返り
VR後のカメラ位置リセット
バトルモードではカメラが固定座標に配置されますが、バトル終了後に探索モードに戻る際、三人称カメラの camera.parent(R3Fのデフォルトグループ)に残った回転と位置がリセットされず、カメラが明後日の方向を向くバグが発生していました。解決策として、バトル終了時とエンディング中の毎フレームで camera.parent.position.set(0,0,0) / camera.parent.rotation.set(0,0,0) を明示的にリセットしています。
GLBモデルの描画チラつき(Z-fighting)
GLBステージ内の路面ストライプやデカールメッシュが、親メッシュとほぼ同じ深度で描画されZ-fightingが発生していました。ADVStage.tsx でメッシュ名に line、cross、decal、stripe が含まれるか transparent なマテリアルを持つ子メッシュに対し、polygonOffset: true / polygonOffsetFactor: -1 を適用してカメラ側に引き寄せることで解消しています。
VRMのGPU資産リーク
ステージ遷移やバトル終了時にVRMモデルが差し替わる際、古いVRMのテクスチャやジオメトリがVRAMに残り続ける問題がありました。ADVAvatar.tsx のクリーンアップで VRMUtils.deepDispose(vrm.scene) を呼ぶことで、VRM関連のGPU資産をすべて解放しています。
連鎖的なsetTimeoutによるフェーズ遷移
エンカウント→バトル→勝利→探索復帰の遷移は、setTimeout の入れ子チェーンで「フェードアウト400ms → 状態切替 → フェードイン600ms」を実現しています。fadeState を useFrame で毎フレームLerpすることで、タイマーとレンダリングの同期を保っています。
箒モード時の頭ボーン逆回転
箒飛行時にアバター全体が前傾すると顔も下を向いてしまう問題を、vrmRef.current.humanoid.getNormalizedBoneNode('head') で頭ボーンを取得し、体の傾きと逆方向に同じ TILT_LERP_FACTOR で滑らかに回転させることで、カメラに対して常に正面を向き続ける設計としました。
LAUNCH_COMMAND: adv
プロトコルに接続せよ。