[Astro #54] LAYER_STG_PROTOCOL —— R3Fによる3Dゲームループの構築

[Astro #54] LAYER_STG_PROTOCOL —— R3Fによる3Dゲームループの構築

はじめに

昨日、AIと壁打ちして構想を練っていた3Dシューティングゲームのベータ版を公開しました。

詳しくは以下の動画を参照ください。

YouTube:

1. コアアーキテクチャと状態管理

本プロジェクトにおけるシューティングゲーム(以下、LAYER_STG_PROTOCOL)のインフラを支える、システム起動シーケンス、3層に分離されたコンポーネント設計、および非ブロッキングなステートマシン駆動による高速リトライの技術的詳細。

1.1 ターミナル起動(WiredTerminal ↔ WiredScene 連携プロトコル)

世界観(Copland OS)を壊していたピンク色のベタ書きデバッグボタン [ DEBUG: MODE = STG ] を画面上から完全にパージし、Webサイト上のコンソールから直接システムをハック・起動するシーケンスへとリファクタリングを行った。

  • イベント駆動によるトグル制御: WiredTerminal のコマンド受付部(switch(cmd))に新規コマンド stg を追加。コマンド実行時にカスタムイベント wired-toggle-stg-mode をグローバルに向けて放出する。これを受け取る親コンポーネント WiredScene.tsx 側に useEffect のリスナーを仕込み、アバターの配置空間のモードステート(appMode)を 'ROOM' から 'STG' へと動的にフリップさせる。
  • 開発効率最大化のためのデフォルト装填: 高速なイテレーション(検証・デバッグ)を可能にするため、開発環境においてはターミナルの入力欄(inputValue)の初期ステートにあらかじめ "stg" の文字列をデフォルトでセット。リロード直後に Enter キーを1ストローク叩くだけで、暗転を挟んで最速でゲームを検証できる環境を構築した。

1.2 フェーズ分離(3層アーキテクチャ設計とデータバイパス)

React Three Fiber (R3F) のコンテキスト(WebGLのCanvasの内部)と、通常のHTML/CSS(Canvasの外部のDOMレイヤー)は、相互に直接ステートを渡し合うのが難しいという構造的制約がある。これを極めて軽量に解決するため、以下の「3つのフェーズ(関心事)」に分離し、グローバルオブジェクト(window)を安全なデータバイパス(共有メモリ)として機能させる3層設計を採用した。

  1. Transform / Input制御層 (STGController.tsx): キーボードのハードウェア入力を毎フレームスキャン(WASD、Shift、Space)。自機(VRMアバター)の座標を state.viewport の限界値(Padding考慮)でクランプし、トランスフォーム(滑空移動や左右旋回時のロールロールクォータニオン)の補間計算を行う。計算された最新の座標やシールドゲージの残量は、毎フレーム (window as any).wiredPlayerPoswiredPlayerShieldGauge へ退避される。
  2. Object Pool / Physics層 (STGManager.tsx): WebGL(Canvas内)の絶対的な主軸。通常弾・チャージ弾・敵インベーダー・固定ピラー・斜め隕石の全オブジェクトプールの配列(useRef)を保持し、メインの更新ループ(useFrame)を回す。window 経由でプレイヤーの座標を参照し、後述する二乗衝突判定をバックグラウンドで高速実行する。
  3. HTML Overlay UI層 (STGUI.tsx): Canvasの前面(zIndex: 1000)に絶対配置(position: 'absolute')された純粋なDOMレイヤー。マウスやキーボードのイベントがWebGL空間のドラッグ操作や射撃操作を邪魔しないよう、pointerEvents: 'none' でクリックを透過させる。内部で requestAnimationFrame による独自の60fps軽量監視ループを回し、window からスコア、オーバーヒートフラグ、ボスHPの生データを吸い上げて、CSSアニメーション付きのサイバーパンクUIへとレンダリングする。

1.3 非ブロッキング状態遷移と「1秒リトライ」システム

ゲームのプレイ進行において、ブラウザのJavaScriptメインスレッドを物理的に停止させる window.alert() 等の同期型ネイティブロックはフリーズの原因となるため完全に追放した。代わりに、非同期に状態を監視し合うステートマシンを構築した。

  • ゲームループを破壊しないガード: ゲームの状態フラグ(window.wiredSTGState)として 'TITLE' | 'PLAYING' | 'GAMEOVER' | 'CLEAR' を定義。STGManager.tsx および STGController.tsxuseFrame ループの最上部には、if ((window as any).wiredSTGState !== 'PLAYING') return; というセキュリティゲート(早期リターン)を設定。これにより、タイトル画面やゲームオーバー時に、背景のグリッドスクロールやアバターのアイドルアニメーションを流したまま、ゲームの物理演算(湧き、激突、移動)だけを完全凍結・解放することを可能にした。
  • 初期化の無限ループバグの解消: リファクタリング中に直面した「タイトル画面でEnterを押してもゲームが始まらず、即座にタイトルへ引き戻される」不具合を解消。原因は、STGUI.tsx 内の useEffect の第二引数(依存配列)に [stgState] を指定していたことで、ステートが PLAYING に切り替わった瞬間にエフェクトが再発火し、冒頭の (window as any).wiredSTGState = 'TITLE' が再実行されてしまうセルフ・カーネルパニック(チャタリング)であった。依存配列を完全な空([])へと修正し、初期化プロトコルをマウント時の「最初の1回」だけに限定。これによって状態遷移の競合が完全に消滅し、被弾(GAMEOVER)やボス撃破(CLEAR)のいかなる局面からでも、Enter キーひとつで全アセット・オブジェクトプールを安全にパージ・初期化し、瞬時に再ゲームインできる「1秒リトライ」システムを盤石なものとした。

2. 自機(魔女)のマルチスレッド・アクション

プレイヤーの入力(WASDでの移動、Spaceでの射撃・タメ、Shiftでの防御)は、すべて STGController.tsx 内で非同期かつ並列(マルチスレッド)に処理される。ここでは各アクションの仕様と、実装時に直面したキーボードイベント特有のバグに対する解決策を解説する。

2.1 CYBER_SHIELD(電脳盾)と長押しチャタリングの完全遮断

[SHIFT] キーをホールドしている間、魔女の正面に魔法陣のシールドを展開し、敵や隕石との激突ダメージを完全に無効化する防御システム。シールド展開中に敵と衝突した場合は、敵を弾き飛ばして撃破(スコア加算)する。

この実装において、2つの致命的な「チャタリング(バタつき)バグ」に直面したが、以下のロジックで完全解決した。

  1. OSのオートリピートによる発火バグの遮断: キーを押しっぱなしにすると、OSの仕様により keydown イベントが連続して自動発行される(オートリピート)。これにより、シールドのON/OFFが1フレーム単位でチカチカと切り替わり、展開SEがマシンガンのように連打される不具合が発生した。これを解決するため、イベントリスナー内に if (e.repeat) return; という1行の絶対的なガードを挿入。OSからの自動連打スパムを完全に黙殺し、純粋な「物理的な押し込みと離し」だけをキャッチする堅牢な入力システムを構築した。
  2. オーバーヒート(ゲージ枯結)時のバタつき防止: シールドは展開中にゲージ(shieldGauge)を消費し、0になると割れる仕様だが、最初は「ゲージが0になる → シールド解除 → 次のフレームで0.1回復する → まだShiftが押されているので再展開 → 即座に0になる」という毎秒60回の無限ループバグが発生した。これを防ぐため、新たに isShieldBroken(オーバーヒート状態)フラグを導入。「ゲージが尽きたらフラグを true にし、物理的に Shift キーから一度指を離すまで絶対に再展開させない」という厳格なクールダウン仕様へと昇華させ、ゲームバランスの破綻を防いだ。

2.2 GIGA_PIERCE(貫通魔弾)と通常弾のチャージシステム

[SPACE] キーの入力時間によって、2種類の攻撃を撃ち分ける。

  • 通常弾(タップ): シアンに発光するリング状のレーザー。敵や破壊可能隕石に当たると相殺されて消滅する(active = false)。固定障害物(ピラー)に当たった場合も壁として吸い込まれる。
  • GIGA_PIERCE(ホールド & リリース): Spaceキーを一定時間(0.2秒以上)押し続けると、アバターの正面で光のパーティクルが収束しチャージが開始される。ゲージMAX(1.0秒)でキーを離すと、巨大な超発光エネルギー光球を射出する。 この魔弾の最大の特徴は「圧倒的な判定の大きさと貫通力」にある。ザコ敵や硬い隕石(HP2)に直撃しても自身の active フラグを折ることなく、そのまま奥へと突き進み、射線上のすべての障害物と敵をまとめて粉砕する(チャージ撃破による特大ボーナススコアも加算される)。後述のボス戦においては、一撃でHPを1/4も削り取る最大のダメージソースとなる。

2.3 マルチスレッド入力の競合解決(アクションの排他制御)

プレイヤーは「移動しながら、シールドを張り、さらにチャージを溜める」という複雑な操作を行う可能性がある。 システム崩壊を防ぐため、STGController.tsx では「シールド展開中(isShieldActive === true)は、チャージ時間のカウントアップをストップし、ショットの発射イベントも発火させない」という排他制御(ミューテックス)を実装。これにより、「防御中は攻撃できない」というトレードオフをプレイヤーに強制し、ただキーを全部押しっぱなしにするだけのプレイングをシステムレベルでブロックしている。

3. タイムライン管理と脅威(エネミー)設計

単調な無限湧きを脱却し、商業アーケードシューティングのような「波(緩急)」を作り出すため、STGManager.tsx 内にタイムライン(ディレクター)パターンを実装した。 useFrame 内で毎フレームの delta(差分時間)を stageTime.current に加算し、その秒数に応じて湧き(スポーン)のアルゴリズムを動的に切り替えている。

3.1 空間を演出する無敵の障害物(ピラー)

インベーダーに加えて、画面奥から手前へと超高速(OBSTACLE_SPEED = 25)で迫りくるスペースハリアー風の無敵障害物を実装(PHASE_1および3で出現)。

  • 描画負荷の極小化: cylinderGeometry を極端なローポリゴン(分割数8)にし、wireframe={true}MeshBasicMaterial で発光させることで、ネオンサイバーな世界観とGPU負荷ゼロを両立。
  • 空間の広域化ハック: 初期実装では自機の目の前(X軸 -4 〜 +4)に密集してしまい、理不尽な壁になっていた。スポーン座標の算出を (Math.random() - 0.5) * 40 へと大幅に拡張することで、自機のはるか左右を巨大な柱がギュンギュンと置き去りにしていく、極上の3Dドライブ感(パースペクティブの強調)を実現した。

3.2 アステロイド・ストーム(破壊可能隕石)の軌道計算と神リバランス

20秒経過時(PHASE_2)から、敵の湧きがピタッと止まり、画面の右上奥から手前左下に向かって巨大なオレンジ色の電脳隕石(icosahedronGeometry 分割数0)が降り注ぐストーム地帯。 ここでは2つの致命的なバグとゲームバランスの崩壊が発生したが、ベクトルの緻密な再計算によって「神調整」へと昇華させた。

  1. 軌道バグの修正: 初期実装ではX軸(左方向)のマイナス速度が速すぎたため、自機に到達する前に画面の遥か左へ「真横に」通り過ぎてしまうただの背景になっていた。奥から手前へ到達する2秒間にちょうど中央(X=0付近)をすり潰すように、met.velocity のX成分を -2.2 〜 -4.7 の範囲へとデチューンし、正確な対空斜め軌道を完成させた。
  2. 「避けられない壁」の解消(サイズと密度のリバランス): スポーン間隔が0.4秒だったため、巨大な岩石が画面を埋め尽くし、どんな凄腕プレイヤーでも即座に大破(カーネルパニック)する無理ゲーと化していた。スポーン間隔を 1.4秒に緩和 し、隕石の半径を 1.4 から 0.85 へ縮小。さらに二乗衝突判定の閾値(dx*dx + dy*dy)を 3.5 から 2.0 へ引き締めることで、「引き付けて通常弾で壊すか、スレスレの隙間を縫って回避するか、シールドで弾くか」というアーケードライクな極上の駆け引き(プレイフィール)を成立させた。

3.3 巨大ボスインベーダー戦(メモリ負荷ゼロの巨体召喚)

55秒に到達(PHASE_BOSS)すると、すべてのザコや隕石の湧きタイマーが強制停止し、画面最奥(Z=-45)に不気味にホバリングする巨大ボスが出現する。

  • スケール合成によるアセット流用: 新しい3Dモデルを読み込んでメモリを圧迫するのではなく、既存のインベーダーモデル(enemyModelRef)を <group scale={[0.8, 0.8, 0.8]}> でラップして流用。元モデルから見て約8倍の全幅7メートル超の超巨体を、ロード時間ゼロ・メモリ追加ゼロで空間に召喚するエコかつ効果的なハックを採用。
  • ヘルス管理とカタルシス: ボス出現中は STGUI.tsx 側に赤い巨大なWARNING HPバーが出現する。HPは40に設定されており、通常弾(1ダメージ)でチクチク削ることもできるが、GIGA_PIERCE(チャージ魔弾)を直撃させると一撃で 10ダメージ(HPの4分の1) を奪い取ることができる。重厚な爆発音とともにボスのゲージがガツッと消し飛ぶ瞬間は、ゲームにおける「タメ技」のカタルシスを完璧に体現している。
  • フリーズの完全消滅: 当初、1分経過時のクリア判定に alert() を使ってしまったことで、WebGLのレンダリングスレッドが物理的にロックされ、画面がフリーズ(暗転)する事故が発生した。ボス撃破時に clearAllObjects() で全プールをパージし、非同期でHTML側の CLEAR ステートを起動するように設計を刷新したことで、60fpsを1ミリも落とすことなく、美しい緑ネオンの「MISSION_CLEAR」画面へとシームレスに着地する完全なエンディングフローを確立した。

4. 極限のパフォーマンス最適化(AIコンテキスト重要事項)

JavaScriptというシングルスレッド環境のブラウザ上で、弾幕や無数のオブジェクトが飛び交う3Dシューティングを 常時60fpsに張り付かせて動作させる ためには、Reactの標準的な作法をあえて破り、Three.jsネイティブな最適化手法を注入する必要がある。 今後の拡張(XR対応や新エネミー追加)において、AIアシスタントや開発者がこのコードベースを読み込む際、以下の3つの「最適化の掟」を厳守すること。

4.1 完全なオブジェクトプール(GCスパイクの完全排除)

弾が発射されるたびに new THREE.Mesh() を生成し、消えるたびに dispose(あるいはReactコンポーネントのアンマウント)を行うと、ブラウザのガベージコレクション(GC)が走り、ゲーム中に致命的なプチフリーズ(GCスパイク)が発生する。

  • 静的アロケーション: STGManager.tsx では、通常弾30発、敵10体、ピラー15本、隕石20個を、マウント時に useRef を用いて固定長配列(Array)として一括生成している。
  • リサイクルの徹底: 以降はメモリの確保・破棄を一切行わない。オブジェクトの生死は論理的な active フラグのON/OFFだけで管理し、描画の表示/非表示は visible プロパティの切り替えのみで行う。これにより、弾幕がどれだけ激しくなってもメモリ使用量は完全にフラットに保たれる。

4.2 平方根(Math.sqrt)を排除した超高速・二乗衝突判定

毎フレーム行われる「自機 vs 敵」「弾 vs 敵」「弾 vs 隕石」などの当たり判定ループは、全オブジェクトの総当たり戦となるため、計算量が爆発しやすい。

  • DistanceToの禁止: Three.js標準の Vector3.distanceTo() は内部で三平方の定理による平方根計算(Math.sqrt())が走るため、毎フレーム数百回呼び出すとCPUの深刻なボトルネックとなる。
  • 二乗和による解決: 本システムでは平方根を一切使わず、XとYの差分を掛け算した二乗和を用いて判定を完結させている。 if (dx * dx + dy * dy < threshold) CPUにとって掛け算は1クロックサイクルで終わる最も軽い処理であるため、隕石や弾の数を今後さらに数十倍に増やしたとしても、判定処理でフレームレートが落ちることは物理的にあり得ない。

4.3 軽量マテリアルによるGPU負荷のゼロ化

通常、3Dゲームが重くなる最大の原因は「光源計算(リアルタイムシャドウや反射)」にある。

  • 光の計算を捨てる: 画面を埋め尽くす巨大ピラーや隕石には、標準的な MeshStandardMaterial ではなく MeshBasicMaterial を採用。このマテリアルはシーン内のライト(光源)を一切無視して指定色を直接画面に描画するため、GPUのライティング計算負荷が実質ゼロになる。
  • ワイヤーフレームの美学: さらに wireframe={true} を適用することで、描画するピクセル数(フィルレート)を劇的に削減。Copland OSのサイバーパンクな「電脳空間」のビジュアルアートスタイルを確立しつつ、圧倒的な軽量化を両立するハックとして機能している。

5. UI/UX と演出

ゲームメカニクスの堅牢さに加え、Copland OS風の「電脳空間への没入感」を最大化するため、音響とUI(ユーザーインターフェース)の演出にも徹底的にこだわった。

5.1 感情をコントロールするBGMトランジション

ゲームの進行状態(State)に合わせて、オーディオのシームレスな切り替えを実装。

  • 静から動へ: タイトル画面(TITLE)では、哀愁漂うアンビエントなBGM(stg_bgm_opening)を再生。プレイヤーが「Enter」キーを叩き、システムがブート(PLAYING)した瞬間に、現在再生中のオーディオを pause() で強制停止し、即座にハイテンポで攻撃的なサイバーテクノBGM(stg_bgm_stage1)へと切り替える。この「静」から「動」へのコントラストが、プレイヤーの戦闘へのモチベーションを一気に跳ね上げる。
  • SE(効果音)のレイヤー制御: 魔法陣の展開音、チャージ音、通常弾の発射音、そして激突時の爆発音が同時に鳴り響いても音が割れないよう、多重再生可能な設計にしている。特にシールド展開中のループ音(se_magic_shield)は、フラグ管理によって「展開中のみ一定間隔で鳴り続ける」よう制御し、視覚だけでなく聴覚でも「今無敵であること」をプレイヤーに確信させている。

5.2 3Dと2Dが融合したサイバーUI(HTML Overlay)

React Three Fiberの最大の強みである「3D Canvasと通常のHTML/DOMツリーの共存」を活かし、ゲームのHUD(ヘッドアップディスプレイ)はすべて STGUI.tsx 側のHTML/CSSで描画している。

  • スキャンラインと発光表現: 「SCORE」や「SHIELDゲージ」は、CSSの text-shadowbox-shadow を多用し、古いCRTモニターで発光しているようなネオン・グロー効果を付与している。また、フォントには等幅の monospace を指定し、システムUIとしての無骨さを表現。
  • 状態に応じたダイナミックなUI変化:
  • OVERHEAT警告: シールドゲージが0になり破損状態(isBroken)に陥ると、ゲージの色がシアン(#00ffff)から警告色のネオンレッド(#ff0055)へとフリップし、文字も < OVERHEATED > に切り替わる。
  • 巨大BOSS HPバー: ボスフェーズ(PHASE_BOSS)突入時のみ、画面中央の最上部という最も目立つ位置に WARNING: BOSS_IDENTIFIED の文字と共に巨大なヘルスバーがスライドインする。ボスのHPの増減(bossHp / bossMaxHp)をCSSの width のパーセンテージ計算に直結させることで、被弾ごとにゲージが削れていくカタルシスを視覚化している。