[Astro #81] 3Dシューティングにおける被弾判定・シールド装甲剥離・ゲームオーバーシーケンスとHMRライフサイクル問題の解決

[Astro #81] 3Dシューティングにおける被弾判定・シールド装甲剥離・ゲームオーバーシーケンスとHMRライフサイクル問題の解決

はじめに

本日の開発では、プレイヤーの被弾判定からゲームオーバーシーケンスの構築、バリアの装甲剥離演出、そして3Dグラフィックス特有の描画およびライフサイクルの不整合修正にまたがる、システムの基盤強化を行いました。以下にその具体的な実装内容を記録します。

1. プレイヤー(自機)の被弾判定エンジンの実装とアライメント微調整

ゲームの根幹となる、プレイヤー(自機)の被弾・衝突判定システムを構築しました。

  • 極小弱点(コア)判定の実装: 3Dモデル全体のバウンディングボックスサイズ(PLAYER_SIZE)ではなく、その中心部(イレイナの胸元付近)に PLAYER_SIZE * 0.35 の球体判定を設定しました。これにより、弾幕をギリギリで避けるスリリングなゲーム性を担保しています。
  • 被弾リザルト処理の分岐: バリア(シールド)展開時はシールド耐久値を減算し、生身の状態で被弾した場合は残機(lives)を減少させた上で、装備中のパワーアップ(スピード、武装、オプション等)をすべて初期化するレトロSTGスタイルのストイックなリセットロジックを配線しました。
  • 復活時の無敵明滅インフラ: 被弾後の復活時には 2.5秒 の無敵時間を設定し、タイマー更新に合わせて1秒間に20回の頻度で自機モデルの visible を交互に切り替える「高速明滅演出」を実装しました。
  • グラフィック追従のミリ単位修正: 箒にまたがる3Dモデルの重心特性に合わせ、バリア(3連ジャイロリング)の展開Y座標に +0.05 軸の上方オフセットを追加し、視覚的なアライメントを完全に一致させました。

2. 耐久力連動型シールド(魔術バリア)の多層リング剥離システム

シールドの残り耐久力(shieldHp)の減少度合いを、プレイヤーへ視覚的にフィードバックする仕組みを最適化しました。

  • 仕様への適合と不整合修正: シールドの最大耐久力を 最大 6(6回被弾まで耐える防壁)と定義し、2回ダメージを喰らうごとに外側から1枚ずつ幾何学リングが剥がれ落ちる仕様を実装しました。
  • 不等式によるステップ表示: 3つのリング(水色・紫・ピンク)に対して visible={shieldHp >= 5}>= 3>= 1 という1対1のステップ不等式を適用しました。
  • Nullエラーの完全封殺: R3F(Three.js)の useFrame 内で常に回転・マテリアルアニメーションが計算されているため、コンポーネント自体を条件分岐で削除(アンマウント)するのではなく、visible プロパティの切り替えによる非表示化を採用しました。これにより、ポインタ参照エラーによるクラッシュを100%防いでいます。
  • ガチンコ仕様へのバランス調整: シールド被弾時のセーフティ無敵時間を 0 秒(無効化)にアジャストしました。敵本体(体当たり)に接触した雑魚敵はその場で即座に爆発・間引き処理(HPを0にして配列から除外)される安全インフラを維持しつつ、巨大なボスや濃密なレーザー・弾幕地帯に突っ込んだ際には一瞬で装甲が削り取られるシビアな緊張感を演出しています。

3. クラシック・ゲームオーバーシーケンスと統合リセット関数の構築

アーケードSTGの美学に則った、ゲームオーバー時の演出およびデータ完全初期化のパイプラインを確立しました。

  • 無情スクロール演出: 残機が0の状態で被弾した際、即座にタイトル画面へ送還するのではなく、背景の地形や敵のスクロール、AIの動作は稼働させたまま、自機グラフィックのみを強制隠蔽(消去)するフェーズへ遷移させます。
  • Html 2D投影インターフェース: 空間中央にネオン調で上下に浮遊バウンドする「GAME OVER」テキストを表示し、専用SEの再生終了に合わせた3秒後に、伝統の1フレーム点滅(Blink)を刻む「- HIT ANY KEY -」をマウントしました。
  • 全宇宙リセット関数の配線: キーボード、マウスクリック、およびVRコントローラーによるタイトル復帰時、Reactのステート(残機・スコア・パワーアップフラグ)だけでなく、3D空間内にアクティブで残存している全オブジェクトバッファ(通常弾、敵弾、敵本体、ミサイル、パーティクル、カプセル等のRef配列)およびステージの進行タイムラインカウンター(timelineIndexRef 等)を完全にゼロへと巻き戻す、一元管理の統合リセット関数(handleReturnToTitle)を構築しました。これにより、再スタート時の不整合バグを完全に撲滅しました。

4. 地形オブジェクト(モミの木)の原点不整合修正

床面(舞台)に配置される地上物オブジェクトの物理判定が、床下に埋まってしまう不整合を解決しました。

  • 原点仕様の不一致問題: 3Dモデル(.glb)の原点が「足元(根元)」にあるのに対し、Three.jsのコライダージオメトリの原点は「中心(ド真ん中)」にあります。そのため、補正なしで重ねるとヒットボックスの下半分が床下に埋まり、上空がスカスカになる現象が発生していました。
  • 中心点シフト & 描画相殺アルゴリズム: 設定データ(JSON)へモデル固有の中心ズレ幅(モミの木は高さ 0.45 の半分である hitOffset: 0.225)を追加しました。オブジェクト生成時に、あらかじめ y 座標にこのズレ幅を合算して「物理コライダーの中心」を obj.y として保持させ、描画(TerrainInstance)に引き渡す直前で obj.y - hitOffset と引き下げることで、グラフィックの接地感を完璧に維持したまま、青いデバッグワイヤーフレームの箱を上空へ正確にジャストフィットさせることに成功しました。

5. Vite HMRにおけるライト多重累積(白飛び・暗黒化)の完全解決

Vite環境でのコード修正保存時(ホットリロード:HMR)に、シーン内に古い環境光や平行光源が登録抹消されずに累積し、画面が段階的に白飛びしていくR3F特有のライフサイクル問題を完全解決しました。

  • JSXタグからプログラム管理への移行: JSX内に <ambientLight /> などを直書きすると、Reactの仮想DOMの差し替えとThree.jsの実態消去の間にタイムラグが生じ、ゴーストライトが虚空に取り残されます。これを解決するため、ライトの配置をJSXから撤去しました。
  • 動的生成 & 重複排除(デデュプリケーション)アルゴリズム: useEffect を用い、生のThree.jsインスタンス(new THREE.AmbientLight 等)としてライトを動的に生成し、直接 scene.add() するインフラへ変更しました。その上で、シーン全域を走査(scene.traverse)し、同じ名前を持つライトオブジェクトが2つ以上に増殖した瞬間を検知して、最も古い残骸オブジェクトだけを狙い撃ちで物理消去(scene.remove)する重複排除スレッドを配線しました。
  • 結果: これにより、通常マウント時には適正な光量(環境光 0.6 / 平行光 0.5)で舞台が鮮やかに照らし出され、かつコードを何十回保存しても輝度が1ミリも変わらない、メモリリークのない鉄壁の開発環境を確立しました。

6. 入力制御の厳密化とデバッグトグルのマウント

細かな操作性の向上と、今後のステージ開発を見据えた快適インフラを追加しました。

  • ゲームオーバー中の射撃遮断: 自機消滅後にスペースキーを連打した際、画面の虚空から通常弾やレーザーが漏れ出して発射されてしまう不整合を、onFire コールバックの最先頭へゲームオーバー状態のガード節(Early Return)を挟むことで完全に遮断しました。
  • リアルタイム・デバッグ枠切り替え機能: 画面上に表示されている物理当たり判定枠(自機:緑のボックス、敵:赤のリング、地形:青のボックス)の表示を司る showHitbox ステートを配線しました。初期値を false(製品版表示)にしつつ、プレイ中にキーボードの H キー を押すだけで、いつでも枠線の有無をノンストップでリアルタイムにトグル切り替えできる快適なデバッグ機能を実装しました。