[Astro #59] WebXRにおける入力同期プロトコルのクレンジング 〜量子爆発ボムの実装と多重トランスフォーム競合の完全遮断〜
はじめに
以前から実装しようと思いつつ実現できなかったボムを実装してみました。
3つのリングが回転し、自機から画面中央へ発射後に拡大するというシンプルな内容です。
他にもいくつかパターンを作ったので、もしアップデートする場合、複合的に組み合わせたアニメーションを作るかもしれません。
発射はBボタンで、VRのボタンもB。
BOMは、3つまで所有可能で、敵を倒した際に1%の確率でドロップします。
ちなみにステージデータも作成したので、あとは、EDを作ったら終了です。
STGコマンド実行時に、PROTOCOL.LAINなど画面情報を非表示にする実装をしたいと思いつつ、何時でも出来る簡単な内容なのでそのままになってます。
面倒な事を先に終わらせてからやるつもりが、いつもそこまで手が回ってないです。
1. 量子爆発(ボムプロトコル)のタイムライン設計
切り札となるボム「Cyber Rings」の実装にあたり、アーケードゲームと同等のカタルシスと安全地帯を確保するため、単なる即時一掃ではなく、「射出移動(フェーズ1)」 と 「ターゲット地点での炸裂(フェーズ2)」 の2段階タイムライン構造を鋳造した。
// タイムラインパラメーターの定義
const TRAVEL_DURATION = 0.6; // 自機位置から目標地点(敵陣中央)への高速移動時間
const EXPLOSION_DURATION = 1.4; // 目的地到達後の大爆発・リング大展開時間
const TOTAL_DURATION = TRAVEL_DURATION + EXPLOSION_DURATION;
フェーズ1:リアルタイム位置トラップと弾道線形補間
ボムが点火(time.current === 0)されたまさにその最初の1フレーム目に、グローバルに共有されている自機(アバター)のグラフィック座標 wiredPlayerPos をトラップし、発射初期位置 startPos にロックする。
if (time.current === 0) {
const playerPos = (window as any).wiredPlayerPos;
if (playerPos) {
startPos.current.x = playerPos.x;
startPos.current.y = playerPos.y - 1.5; // 接地/浮遊の目測オフセット補正
}
}
そこから TRAVEL_DURATION(0.6秒)の間、lerpVectors を用いて、初期位置から画面中央奥の固定ターゲット座標([0, -2, 0])へ向けて魔法球を高速で直線補間(lerp)させる。この移動フェーズ中は、画面全体の敵や弾幕の一掃判定(持続ダメージ)は起動せず、先行アニメーションに専念させることで演出のタメを作る。
フェーズ2:炸裂同期と持続消去プロトコル
タイマーが TRAVEL_DURATION を超えた瞬間、一掃判定フラグを反転させる。
(window as any).wiredBombExploding = true;
マネージャー側(STGManager.tsx)はこの電波を受信し、爆発が持続している1.4秒間、毎フレーム敵の弾プールを強制的にシャットダウン(active = false)し、ザコ敵および隕石、ボス本体に対して強力な持続ダメージを叩き込む。これにより、着弾の瞬間に画面の脅威が同時に木っ端微塵に爆散する、完璧な安全地帯の展開に成功した。
2. 多重上書き(ゾンビ回復バグ)の発生メカニズム
WebXR(VRゴーグル内でのプレイ)の実装時、PCブラウザ(キーボード)では完璧に動作していた「シールドのオーバーヒート(破損ロック)機構」が、VR空間でのみ完全に無視され、ゲージが0になっても無限に電脳盾が展開され続ける致命的なバグが顕在化した。
この原因をハッキングした結果、上流コンポーネント(WiredScene.tsx / STGManager.tsx) と 下流の自機コントローラー(STGController.tsx) の間で、グローバル変数を経由した「入力フラグの潰し合い(多重代入)」が発生していた。
混線シークエンス
- 下流の
STGController.tsx側でゲージが尽き、オーバーヒートを検知してシステムを安全にシャットダウンする(isShieldActive.current = false)。 - しかし、上流の
WiredScene.tsx(あるいは旧STGManager.tsxの内部ループ)が、VRコントローラーの物理的なグリップボタンの押下を検知し、「物理的に握られているからシールドはON(true)である」と、下流の判定結果を無視して毎フレーム上書き書き戻しを行っていた。 - 次のフレームの初手で、下流コントローラーは上書きされた
trueを見てしまい、「新しくシールド要求が来た」と誤認してオーバーヒートロックを強制解除する。
この結果、「0になった瞬間に1フレームだけ盾が消え、即座に自動回復した微小なエネルギー(1フレーム分)を消費して再展開する」 という、0付近でのゾンビ継続ループ(ガタガタ明滅する無限展開)が引き起こされていた。チャージタイマーが上限値を無視して無限に肥大化していたのも、全く同じ「複数ファイルで個別にタイマーを回して上書きし合っていた」という配線の混線が原因であった。
3. アーキテクチャのクレンジング:下流への入力一本化
この競合状態を根本から駆逐するため、上流の Scene や Manager レイヤーから「状態を計算してグローバルに直接上書きする処理」を根こそぎ全消去(クレンジング)した。
役割を以下のように厳密に分離・リファクタリングした:
- 上流(
WiredScene.tsx): 純粋にコントローラーから吸い上げた「物理的なボタン要求(Raw Input)」だけを、他のシステムを汚さない独立した専用のグローバルキー(wiredPlayerVrGripRequested/wiredPlayerVrTriggerRequested)へ転写・報告することに専念。 - 下流(
STGController.tsx): キーボードの入力(Shift/SPACE)と、上流から流れてくるVRコントローラーの物理要求を「ここで初めて合流」させ、自機内の1本のタイムラインループの管轄下で、すべての消費・蓄積・破損ロックの計算を統合管理する。
// 下流(STGController.tsx)での完全な一本化と、VR遅延を考慮した閾値防壁
const isVrGripRequested = (window as any).wiredPlayerVrGripRequested || false;
const isShieldRequested = keys.current['shift'] || isVrGripRequested;
// VRの離検知フレーム遅延を完全に相殺するため、回復閾値を設けてゾンビ化を徹底ガード
const SHIELD_RECOVER_THRESHOLD = 30;
if (!isShieldRequested && shieldGauge.current >= SHIELD_RECOVER_THRESHOLD) {
isShieldBroken.current = false; // ゲージが30%まで安全に回復するまでロックを解除しない
}
このリファクタリングにより、物理入力とシステム状態の因果関係がクッキリと整理され、VR空間内での移動性能、チャージの最大値ストップ(完了SEの1回再生)、および0での完全なオーバーヒート強制解除がPC版と1ミクロンの狂いもなく完全同期を達成した。
4. VR専用ミニマルホログラムHUDの統合
2D用のステータス画面をVR空間にそのままレンダリングすると、DOMとTextオブジェクトの内部競合によるクラッシュ、およびスケール崩れを引き起こす。
不要なバックパネルや枠線、外部fetchバグの元となる font="monospace" のURL解決仕様をすべて削ぎ落とし、state.gl.xr.isPresenting ガードを敷くことで、「VRゴーグルを被って潜っている時だけ、アバターの右肩上に極小サイズ(fontSize: 0.045)でマウントされるSF風のHUD」を実装した。
useFrame((state) => {
// 2D(PCブラウザ)表示中は根こそぎ存在を非表示化
if (!state.gl.xr.isPresenting) {
uiGroupRef.current.visible = false;
return;
}
// 毎フレーム自機の位置(playerPos)の右斜め上に完全追従
if (playerPos) {
uiGroupRef.current.position.set(playerPos.x + 0.4, playerPos.y + 1.6, playerPos.z - 0.2);
}
});
厳密な3個制限となったボムの残弾数(💣)、電脳盾の残量、ライフが、アバターの激しいステップに遅延なくホバー移動で追従するようになり、ワイヤードの深層レイヤーにふさわしい、最高に没入感の高いコックピットHUDプロトコルがここに開通した。