[Web App] LAYER_ADV_PROTOCOL ─ R3Fで作ったVR対応 3D ADV/RPG

[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)の両方でプレイ可能です。

[Web App] LAYER_ADV_PROTOCOL ─ はじめに

Youtube紹介動画

LAUNCH_COMMAND: adv

serial experiments lain 世界観 × 3D自由探索 × ターン制RPG


設計思想 ─ 異層探索RPG

他の3タイトル(STG、Witchadius、PUZZLE)がアクション/パズルの瞬発力型だったのに対し、ADV Protocolは「空間を歩き、人と話し、敵と戦い、物語を進める」時間経過型の体験を目指しています。

[Web App] LAYER_ADV_PROTOCOL ─  異層探索RPG [Web App] LAYER_ADV_PROTOCOL ─  異層探索RPG

ゲーム状態は EXPLORE(探索)/ BATTLE(バトル)/ ENDING(エンディング)の3フェーズで管理され、探索中にエンカウント条件を満たすとフェードアウト→バトルステージへ遷移、勝利すると元の座標に復帰する構造です。

[Web App] LAYER_ADV_PROTOCOL ─  異層探索RPG

すべてのステージ・NPC・敵・アイテム・成長テーブル・魔法・効果音はJSONファイルで外部定義されており、コードを触らずにゲームバランスの調整やステージ追加が可能です。


ストーリー進行 ─ storyStep駆動

ストーリーは storyStep(0〜4)の整数値で管理されます。

特定のステージに入る、ボスを倒すなどの条件でstepが進行し、NPC会話テキスト・ワープポイントの表示/非表示・ボスNPCの出現がステップに連動して切り替わります。

[Web App] LAYER_ADV_PROTOCOL ─  ストーリー進行 ─ storyStep駆動 [Web App] LAYER_ADV_PROTOCOL ─  ストーリー進行 ─ storyStep駆動
stepトリガー変化
0→1大聖堂(STAGE_CATHEDRAL)に初入場森にボス「HACKLOAD」が実体化
1→2HACKLOAD撃破街のNPC会話が変化(感情が戻る)
2→3大聖堂に再入場地下洞窟(STAGE_CAVE)へのワープゲートが開通
3→4ラスボス「AKUMA」撃破ボス撃破ダイアログ→エンディングシーケンス起動

NPC会話は STORY_NPC_DIALOGUES マップで npcId × storyStep の組み合わせごとに個別テキストを定義しています。


探索モード ─ EXPLORE

GLBステージ

5つの探索ステージがGLBモデルとして用意され、Stages.json に定義されています。

ステージ名前スケール制作者
STAGE_MY_ROOMIreina’s room1.0Katydid.
STAGE_CITYCity12.0Nioma van der Steen
STAGE_CATHEDRALCathedral1.0Yu Kai Him Otto
STAGE_FORESTForest7.0Katydid.
STAGE_CAVEUnderground cave1.0Katydid.

各ステージにはBGM、スポーン座標、カメラ半径、フォグ設定、NPC配置、ワープポイント、セーブポイント、回復ポイント、エンカウント設定が個別に定義されています。

FPS移動 ─ ADVController.tsx

探索中のプレイヤー操作はポインターロック+WASD移動のFPS方式。three-mesh-bvh によるBVH(Bounding Volume Hierarchy)加速レイキャストで地面追従と壁当たり判定を実装しています。

パラメータ説明
MOVE_SPEED4.0通常移動速度
BROOM_SPEED8.5箒飛行時の移動速度
PLAYER_HEIGHT1.4プレイヤーの身長(レイキャスト基準)
MAX_STEP_HEIGHT0.6段差の最大乗り越え高さ
WALL_OFFSET0.4壁との最小距離

地面追従は下方向レイキャスト、壁判定は前方レイキャストで処理。地面の高さ変化は GROUND_LERP_SPEED: 0.15 の補間で滑らかに追従し、段差のデッドゾーン(0.05)以下の微小変化は無視してチラつきを防いでいます。

スペースキーでジャンプ(初速5.2、重力加速度-9.8)が可能で、接地判定は下方レイキャストの距離で判断します。

ワープポイントとNPC

ワープポイントは WavyRing(波紋リング+パーティクル)で視覚化され、プレイヤーが近づくとフェードアウト→ステージ遷移→フェードイン。NPCは各ステージの npcs 配列で定義されたVRMモデルが固定位置に立ち、近づくと会話テキストが表示されます。

[Web App] LAYER_ADV_PROTOCOL ─ 探索モード ─ EXPLORE / ワープポイントとNPC [Web App] LAYER_ADV_PROTOCOL ─ 探索モード ─ EXPLORE / ワープポイントとNPC

セーブ/ロード ─ SaveMenu

セーブポイント(紫色のWavyRing)に近づくと SaveMenu が開き、IndexedDBにプレイヤーレベル・経験値・HP/MP・ストーリー進行度・現在ステージ・座標・向きを永続保存。ロード時はすべての状態を復元してプレイを再開できます。

[Web App] LAYER_ADV_PROTOCOL ─ 探索モード ─ EXPLORE / セーブ/ロード ─ SaveMenu [Web App] LAYER_ADV_PROTOCOL ─ 探索モード ─ EXPLORE / セーブ/ロード ─ SaveMenu

回復ポイント

自室のベッド付近に設置された回復ポイント(エメラルド色のWavyRing)に近づくと回復メニューが開き、「YES」を選ぶとフェードアウト→BGM停止→5秒間の暗転(睡眠SE再生)→HP/MP全回復→フェードインの演出が再生されます。

[Web App] LAYER_ADV_PROTOCOL ─ 探索モード ─ EXPLORE / 回復ポイント [Web App] LAYER_ADV_PROTOCOL ─ 探索モード ─ EXPLORE / 回復ポイント

箒飛行モード

レベルアップで MAGIC_BROOM スキルを習得すると、Bキー(VRではグリップ)で箒飛行モードを切り替え可能。アバターが前傾姿勢(-45度、TILT_LERP_FACTOR: 0.15 で滑らかに補間)になり、頭(Headボーン)は逆回転してカメラ正面を向き続けます。移動速度は通常の約2倍(8.5 vs 4.0)。WiredAccessory コンポーネントが箒GLBモデルをVRMの手ボーンにアタッチします。

[Web App] LAYER_ADV_PROTOCOL ─ 探索モード ─ EXPLORE / 箒飛行モード [Web App] LAYER_ADV_PROTOCOL ─ 探索モード ─ EXPLORE / 箒飛行モード

バトルシステム ─ ADVBattle.tsx

ターン制コマンドバトル

バトルフェーズは PLAYER_TURNPLAYER_ANIMENEMY_TURN → (ループ or BATTLE_END)の4状態。

プレイヤーターンでは3つのメインコマンドから選択します。

コマンド動作
ATTACK(たたかう)通常攻撃。魔法弾がプレイヤーから敵へ飛び、着弾でダメージ
MAGIC(まほう)サブメニューに遷移。習得済み魔法の一覧から選択
ESCAPE(にげる)戦闘から離脱。元の探索座標に復帰
[Web App] LAYER_ADV_PROTOCOL ─ バトルシステム ─ ADVBattle.tsx

魔法システム ─ Magic.json

魔法は Magic.json で定義され、レベルアップ時に PlayerGrowth.jsonskills 配列に追加されます。

魔法効果タイプMP消費概要
MAGIC_SHOTATTACK_ONLY5マジックショット(通常魔法弾)
MAGIC_HEALHEAL10HP30回復
MAGIC_LIGHTPASSIVE0暗いステージで周囲を照らすライト
MAGIC_BROOMTOGGLE_MODE0箒飛行モードの切り替え
MAGIC_METEORATTACK_ONLY20メテオストライク(高火力魔法)
MAGIC_STARDUSTATTACK_ONLY15星屑の系譜(5発の追尾星弾)
MAGIC_HEAL_PLUSHEAL20HP60回復

攻撃魔法はそれぞれ独立したエフェクトシステム(ADVChargeSystemADVMagicBurstSystemADVStardustTrail)で演出されます。

[Web App] LAYER_ADV_PROTOCOL ─ 魔法システム ─ Magic.json

魔法弾エフェクト ─ MagicProjectile.tsx

通常攻撃の魔法弾は MagicProjectile コンポーネントが始点から終点へ線形補間で飛行する球体メッシュ。速度2.2(約0.45秒で到達)、AdditiveBlending で発光。着弾時に onHit コールバックでダメージ処理が発火します。

[Web App] LAYER_ADV_PROTOCOL ─ 魔法弾エフェクト ─ MagicProjectile.tsx

星屑の系譜 ─ ADVStardustTrail.ts

5発の星型弾(ExtrudeGeometry の五芒星)がベジエ曲線を描いてランダムな放物線を飛び、敵に収束着弾。各弾には0.38秒の連射ディレイ、ランダムな制御点による曲がり、自転アニメーション。軌跡にはカラフルな星屑パーティクル(4色のシアン・黄・緑・紫)がドロップされます。

発射と同時に足元に4重回転魔法陣が展開。全弾着弾後に自動クリーンアップされます。

[Web App] LAYER_ADV_PROTOCOL ─ 魔法弾エフェクト ─ MagicProjectile.tsx

敵の暗黒魔法 ─ EnemyDarkMagic.ts

ラスボスの特殊攻撃。2フェーズ構成で、CASTING(1.0秒の詠唱チャージ球体)→ SOUL_CHASE(4体の人魂がプレイヤー胸元を高速公転しながら収束→着弾)。人魂はワイヤーフレームの二重球体で、軌跡にディープブルーの火の粉パーティクルが立ち上ります。

敵の特殊攻撃レジストリ

敵ごとの特殊攻撃は specialAttackRegistry マップでシステムクラスにルーティングされ、JSONで定義された "type" 文字列とエフェクトシステムが完全に分離されています。

typeシステムクラス演出
APPLE_DROPEnemyAppleDropリンゴが上空から降り注ぐ
REFLECTION_ARROWEnemyReflectionArrow7本の矢が反射しながら飛来
DARK_MAGICEnemyDarkMagic人魂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 でメッシュ名に linecrossdecalstripe が含まれるか transparent なマテリアルを持つ子メッシュに対し、polygonOffset: true / polygonOffsetFactor: -1 を適用してカメラ側に引き寄せることで解消しています。

VRMのGPU資産リーク

ステージ遷移やバトル終了時にVRMモデルが差し替わる際、古いVRMのテクスチャやジオメトリがVRAMに残り続ける問題がありました。ADVAvatar.tsx のクリーンアップで VRMUtils.deepDispose(vrm.scene) を呼ぶことで、VRM関連のGPU資産をすべて解放しています。

連鎖的なsetTimeoutによるフェーズ遷移

エンカウント→バトル→勝利→探索復帰の遷移は、setTimeout の入れ子チェーンで「フェードアウト400ms → 状態切替 → フェードイン600ms」を実現しています。fadeStateuseFrame で毎フレームLerpすることで、タイマーとレンダリングの同期を保っています。

箒モード時の頭ボーン逆回転

箒飛行時にアバター全体が前傾すると顔も下を向いてしまう問題を、vrmRef.current.humanoid.getNormalizedBoneNode('head') で頭ボーンを取得し、体の傾きと逆方向に同じ TILT_LERP_FACTOR で滑らかに回転させることで、カメラに対して常に正面を向き続ける設計としました。


LAUNCH_COMMAND: adv プロトコルに接続せよ。