雨避けMRゲーム
PICO 4 Ultra上で動作するWebXR製の雨避けMR(複合現実)ゲーム。
概要
プレイヤーはARで現実世界に降る、ゆっくりとした雨を避け続ける。
クリア後、自分のプレイを倍速リプレイとして振り返ると、まるで現実の速さで降る雨を避けていたように見えることで、あの頃の夢を叶えられる!
役割分担
- 田中(ゲーム処理):物理・当たり判定・状態管理・録画・リプレイロジック・WebXRトラッキング
- 一柳(演出):雨の見た目・アバター・UI/HUD・SE・リプレイ画面演出
2つの担当が直接依存しないよう、イベントとデータの一方向受け渡しで疎結合に接続している(main.js のみが両方を import して配線する)。
AI開発ツール分担
- Claude Code:設計・デバッグ・複雑なロジック(core/ 中心)
- OpenAI Codex:コンポーネント量産・繰り返し実装(presentation/ 中心)
プロダクトのフロー
START --(トリガー)--> READY --(3秒後、自動)--> PLAYING
|
(制限時間30秒 or 3ライフ喪失)
↓
CLEAR / GAMEOVER
|
(演出の余韻の後、自動)
↓
RESULT ⇄ REPLAY
|
(左トリガー: 終了)
↓
START(再挑戦)
使用技術
このプロダクトはブラウザ上で完結するクライアントサイドのWebXRアプリケーションで、バックエンドは存在しない(サーバーサイドのロジックやデータベースは不要)。
- フレームワーク:Three.js + WebXR Device API
- 雨描画:Three.js InstancedMesh
- XRセッション:
immersive-ar(MRパススルー)
- トラッキング:WebXR(頭 + コントローラー2本)
- ビルド/開発サーバー:Vite(HTTPS必須)
- デプロイ:Vercel または ngrok(静的ホスティングのみ)
- 言語:JavaScript (ES Modules)
- 対象デバイス:PICO 4 Ultra
現在できている機能
- タイトル画面の演出:プレイヤーの実際の部屋に雨が止まって浮かび、タイトルへ変化してからゲームが始まる導入シーン
- 雨をランダムに降らせ、頭・手の球コライダーで当たり判定
- 垂直モードと斜めモードを一定間隔で(なめらかな遷移つきで)切り替え、避け方にバリエーションを持たせている
- 3ライフ制、30秒生存でクリア/被弾3回でゲームオーバー
- 3秒の準備カウントダウン(READY状態)
- 被弾時:画面フラッシュ+SE+コントローラー振動(左右両方)
- 効果音:雨ループ・被弾・クリア・ゲームオーバーの4種(すべてCC0素材をピッチ・音量調整して使用)
- 頭・両手の当たり判定範囲を半透明球で可視化
- プレイ内容(頭・手・雨の位置、被弾イベント)を毎フレーム記録
- リプレイをARのまま人型アバターで再生(頭にモデルを追従させ、両腕は2ボーンIKで手の位置・向きに合わせる/脚は固定ポーズ)。記録データをなめらかに補間して倍速再生し、被弾演出もリプレイ中に再現
- リプレイの演出強化(
ReplayFX):カメラ追従のスピード線、開始の一撃(白フラッシュ+広がるショックウェーブ+ビネット)、頭・両手の残像、足元の衝撃波リング(頭の上下動から踏み込みを検出)
- 本物の天気(風速・風向)をリアルタイムで取得し、ゲーム内の斜め雨の向き・強さに反映(詳細は下の専用セクション)
- ゲームモード「殴り飛ばす」(START画面の左トリガー or
?mode=swat):手は被弾せず、手の周りの雨粒を弾き飛ばす(頭のみ被弾)。詳細は下の専用セクション
- リプレイ中は雨ループのピッチと音量も上げ、「本物の速さになった」ことを音でも補強
- Vercel 用のデプロイ設定(
vercel.json)を用意(発表用の公開URL向け)
- HUD:視界追従、ハートアイコン・タイマー表示(リプレイ中は進行バーも表示)
- RESULT画面(クリア/ゲームオーバー+スコア・ランク表示)→ リプレイ視聴 or 再挑戦を選択可能
- MRパススルー背景+雨のCGオーバーレイ
こだわりポイント・工夫した点
- 「あの頃の夢を叶えたい」というコアコンセプトの作り込み:プレイ中はゆっくり見える雨が、リプレイでは現実の速度に一致するよう倍速再生され、本物の雨を避けているような体験ができるようにした
- リプレイのなめらかな補間再生:記録は毎フレーム(約72〜90Hz)、再生は約6.4倍速という条件下で、単純なフレームスナップだとアバターや雨がワープして見えてしまう問題があったため、隣接フレーム間を線形補間して滑らかな動きを再現している
- 見た目と当たり判定の一致:雨粒の見た目のサイズと当たり判定のサイズを共有定数で揃え、「見た目では避けたはずなのに当たった」という理不尽さを排除した
- 多段ヒットの防止:被弾した雨粒をその場に残したままにすると、1粒に何フレームも当たり続けてライフが瞬時にゼロになるバグがあったため、被弾した雨粒は即座に再出現させるようにした
- リプレイでも被弾の瞬間がわかる:プレイ中の被弾演出は一瞬で見逃しやすいため、リプレイ再生中にも同じ被弾演出(フラッシュ・SE・振動)を再現し、「なぜGAMEOVERになったのか」を振り返れるようにした
- 雨の降り方の作り込み:雨粒を一気に出現させるとプレイ開始直後から「囲まれている」ように見えたり、地面到達のタイミングが揃って「段階的に降ってくる」ように見えたりする問題があったため、時間をかけて少量ずつ投入する方式に調整した
- 「ほぼ動かずにクリアできる」を防ぐ設計:垂直方向の雨だけでは体をわずかにずらすだけで避けられてしまうため、斜め方向の雨を「風向きの変化」としてなめらかに混ぜ、実際に動いて避ける必要性を作った
- 上記はいずれも実機(PICO 4 Ultra)での試遊とフィードバックを繰り返しながら調整した結果であり、机上の設計だけでは気づけなかった問題点への対応を重視した
中間審査のフィードバックを受けた改善(同日中)
- 「リプレイをもっとカッコよく(モーションブラー・スピード感・“マトリックスみ”)」 → 新モジュール
ReplayFX を追加。スピード線・突入の一撃・アバターの残像・足元の衝撃波を重ねた(アバターの脚ボーンは触らず“見た目”だけで下半身を処理)
- 「雨をよける時の動きに変化を」 → 本物の天気の風向・風速をゲーム内の斜め雨に反映。外の天気で避け方が変わる
- リプレイ中の雨音もピッチアップし、「速さ」を音でも表現
本物の天気で雨の向きが変わる(技術の無駄遣い枠)
わずか30秒のゲームのために、わざわざリアルタイムの気象データを引いてきて雨の降り方に反映している。
- データ元:Open-Meteo(APIキー不要・CORS開放・無料)。会場(北九州)の緯度経度を焼き込み、
current=wind_speed_10m,wind_direction_10m を取得。展示中も5分ごとに取り直して「今この瞬間の風」に追従する
- 風向の変換:気象の「風向」は“風が吹いてくる方角”なので、ゲーム内で雨が流されていく向きは +180°。斜めモードのとき雨はこの方角へ流れ、傾き角は実風速から決める(無風で約6°、6m/sで約30°、上限45°)
- WebXRには方位磁石が無いという壁:XRセッションの座標軸が真北から何度ズレているか分からない。合わせ方を2つ用意した——
?north=NN で手動指定するか、ヘッドセット内で矢印を出して「真北を向いてグリップ」を握り、その場の頭の向きを北の基準として記録する
- 符号の検算UI:
?wxdebug を付けると、?wind=&dir= を回したときに「風向を時計回りに変えたら雨の流れ先も時計回りに回るか(逆なら ✗ズレ)」をカードに表示。実機で符号を一発確認できる
- 無駄っぷり:晴れの日は無風でほぼノーヒットになる。誰も得しない。でも「降ってもいない雨をよける妄想」に現実の風を吹かせるのは on-theme なのでやる
ゲームモード「殴り飛ばす」
原モード(よける)はそのまま残しつつ、opt-in の別モードとして追加。妄想がカンフー映画寄りになる(弾よけ → 弾いなし)。
- 切り替え:START画面の左トリガー(右トリガー=開始)。または
?mode=swat で最初からこのモード
- ルール:手は被弾しない。代わりに手の周り(半径0.18m)に入った雨粒を弾き飛ばす。被弾するのは頭だけ
- 手応え:弾いた瞬間に 短い振動+高ピッチの「バシッ」音+加算ブレンドの飛沫バースト。リプレイでも同じタイミング・位置で再現(記録データに swat を追加)
- 当たり判定の可視化も切り替わる(頭=守る色/両手=アクション色・大きめ)
- 原モードは1行も挙動を変えていない(
?mode=swat 無しなら swat の判定・イベント・記録はすべてスキップ)
詰まった点・ハマった点
- WebXR には体・脚をトラッキングするAPIが無い:ブラウザが受け取れるのは頭とコントローラー(+手)だけで、腰や脚を表す仕組みが規格自体に存在しない。リプレイアバターの脚を実データで動かす方法がなく、「脚は固定ポーズ+頭と手からの近似」に落ち着いた。PICO純正トラッカーを使うにはブラウザ外のネイティブ連携が別途必要だと分かった
- ネイティブ連携(PICO Motion Tracker)も試したが、結局リアルタイムでは使えなかった:補助アプリ(Unity)と PICO Browser が同時に XR セッションを持てないというプラットフォーム制約に突き当たった。ローカル WebSocket のブリッジまで実装したが、プレイ中の実データ連携は断念し、リプレイの下半身は演出(残像・足元の衝撃波)で振り切る方針に切り替えた
- 現実の風向をゲームに合わせるのが難しい:WebXR は方位磁石を提供しないため、取得した気象の風向をそのままゲーム世界へ置いても「どっちが北か」が確定しない。ブース向けに「向いてスタート=その向きを北とみなす」キャリブUIを別途用意して回避した
- リプレイの座標系は「XRセッションを切らない」のが絶対条件:
local-floor 参照空間は同一セッション中だけ原点が固定される。プレイ中に記録した頭・手・雨の座標をリプレイでそのまま再配置する設計なので、途中でセッションが切れると全部ズレる。ゲーム→リプレイ→再挑戦を1セッションで完結させることで担保した
- glTF読み込みでボーン名のドットが消える:
Shoulder.L のような名前が読み込み時に ShoulderL へ自動変換され、名前でボーンを取得するコードが軒並み失敗。アバターが「直立のまま平行移動するだけ」になり、原因特定に時間がかかった。名前解決にフォールバックを入れて解消
- 読み込むモデルの実寸がバラバラ:使用したモデルは頭までの高さが約4mあり、想定していたスケール補正の範囲を超えてアバターが巨大化、足が床に埋まった。モデルのサイズに依存せず「目の高さ」を基準に自動スケールする方式へ変更
- アニメ再生中の姿勢補正が毎フレーム累積する:しゃがみに合わせて膝ボーンを回転させたら、アイドルアニメがそのボーンをリセットしないため回転が積み上がり脚が破綻。毎フレーム基準姿勢から作り直す実装に修正
- 液体表現のタイトルが「白い塊」で読めなかった:文字の縁取りを太くしすぎて字の内側が潰れ、さらにシェーダの縁光が内部まで明るくしていた。縁取りを最小限にし、光を上端・下端だけに絞って可読性を確保
- ヘッドセット内ではDOMオーバーレイが不安定:HUDをHTMLで重ねる案は表示が乱れたため、CanvasTextureを貼った3Dパネルを視界に追従させる方式へ切り替えた
- 実機を触れるのが一人だけ:MRの見た目は実機でしか確認できず、もう一方はヘッドセットを持っていない。描画結果をピクセル単位で読み出して数値で検証したり、デスクトップ用の確認ページを別途用意したりして、実機待ちの停滞を減らした
今後の展望
- 傘などの防御アイテム・追加インタラクション
- 時間経過による難易度上昇(雨が徐々に増える等)
- リプレイの一時停止・巻き戻し・速度変更UI
- BGMの追加
- リプレイの下半身:頭・手の動きからの疑似足アニメーション(実データ連携はプラットフォーム制約で不可のため、演出方向で継続)
- 本物の天気に降水量も反映(現在は風速・風向のみ)