[Astro #91] LAYER FIGHTER // リファクタ・Canvas統合・VR対応土台・バトルUI刷新
はじめに
前回、[Astro #90]でバトルシステムを強化した。
[Astro #90] LAYER FIGHTER // バトル強化 - 被弾アニメ・プレイヤーKO・敵攻撃バリエーション・2本先取 // PROTOCOL.LAIN
React Three Fiber + VRM で格ゲー「LAYER FIGHTER」のバトルシステムを強化。被弾のけぞりアニメ(SYS_HURT)、HITSTUN中の入力キャンセル防止、プレイヤーKO判定とYOU LOSE表示、敵攻撃をAssets.jsonで管理するバリエーション機能、2本先取の勝利条件まで実装した記録。
lain-lab.com今回はひたすら技術的負債の返済に集中した回だ。
実装した内容は多岐にわたる。
1. バグ修正
しゃがみ中のヒットボックスY軸判定
しゃがんだ状態で敵のパンチを食らうとダメージを受けるバグを修正した。
原因はヒット判定がX軸しか見ていなかったこと。プレイヤーのしゃがみ状態に応じてハートボックスのY範囲を変えるようにした。
const isPlayerCrouching =
stateRef.current === 'CROUCHING' ||
stateRef.current === 'CROUCH_GUARDING' ||
stateRef.current === 'CROUCH_PUNCH';
const defMaxY = isPlayerCrouching ? 0.8 : 1.6; // しゃがみ時は上半身が消える
const atkMinY = hb.offsetY - hb.height / 2;
const atkMaxY = hb.offsetY + hb.height / 2;
if (
atkMaxX > defMinX && atkMinX < defMaxX && // X軸
atkMaxY > defMinY && atkMinY < defMaxY // Y軸
) {
PUNCH_L のヒットボックスは offsetY: 1.1 なので、しゃがみ時(defMaxY=0.8)はスカになる。
多段ヒットバグ修正
パンチや攻撃を連打すると相手に複数回ヒットしてライフが一気に減る問題を修正した。
敵側には ep.hasHitPlayer による1回ヒット制御があったが、プレイヤー側にはなかった。
// 変更前
if (isAttacking && move && ep.hitstun === 0) {
// 変更後
if (isAttacking && move && ep.enemyState !== 'HITSTUN') {
2. 敵ステートマシン化
前回まで enemyHitstunAnimRef(暫定対処)で被弾アニメを管理していた。これをプレイヤーと同様のステートマシンに置き換えた。
EnemyPhysics に enemyState を追加:
type EnemyState = 'IDLE' | 'WALKING' | 'COOLDOWN' | 'ATTACKING' | 'HITSTUN';
interface EnemyPhysics {
enemyState: EnemyState;
hitstunTimer: number; // HITSTUNの残りフレーム
// ...
}
AIブロックを switch(ep.enemyState) に一本化:
switch (ep.enemyState) {
case 'HITSTUN':
ep.hitstunTimer--;
if (ep.hitstunTimer <= 0) {
ep.enemyState = 'IDLE';
setEnemyAnimId('IDLE_CHANGE_POSE');
}
break;
case 'ATTACKING':
if (!ep.attackMoveId) {
ep.enemyState = 'COOLDOWN';
ep.aiCooldown = 60;
}
break;
case 'COOLDOWN':
ep.aiCooldown--;
if (ep.aiCooldown <= 0) ep.enemyState = 'IDLE';
break;
case 'IDLE':
case 'WALKING':
default:
// 距離判定で接近・攻撃・後退
break;
}
enemyHitstunAnimRef は完全に削除した。
3. リファクタ:ファイル分割
FTGTrainingScene.tsx が約1841行になっていたため、段階的に分割した。
分割前:
└── FTGTrainingScene.tsx(1841行)
分割後:
├── FTGTrainingScene.tsx(586行) エントリー + DOM HUD
├── FTGComponents.tsx(693行) 3Dコンポーネント群
└── useFTGGameLoop.ts(790行) ゲームループ
Astro + Viteの環境では .ts / .tsx ファイル間での型のre-exportが不安定なため、各ファイルで型定義をコピーする方針にした。
4. Canvas統合(VR対応土台)
最大の技術的負債だった。FTGManager が WiredScene のCanvas外に置かれていたため、WebXRのVRセッションが共有できない構造になっていた。
WebXRの仕様として、VR Sessionは1つのWebGL Context(= 1つのCanvas)に紐付く。Canvasが複数あると、どれか1つしかVRにできない。
修正前:
WiredScene <Canvas>(XR対応済み)
FTGManager(Canvas外)
├── FTGTitleBackground <Canvas>(独自)
└── FTGTrainingScene <Canvas>(独自)
修正後:
WiredScene <Canvas>(XR対応済み)
└── appMode === 'FTG'
└── FTGManager
├── FTGTitleBackground(Canvas除去)
├── FTGTrainingScene(Canvas除去)
├── FTGCreditSequence(Canvas除去)
└── FTGSoundTest(Html対応)
各コンポーネントからCanvasを除去し、カメラ設定は useThree で制御する形に変更した。
// FTGTitleBackground.tsx
const { camera } = useThree();
useEffect(() => {
camera.position.set(-9, 9, 11);
(camera as THREE.PerspectiveCamera).fov = 22;
(camera as THREE.PerspectiveCamera).updateProjectionMatrix();
return () => {
// 終了時にリセット
camera.position.set(0, 0, 4);
(camera as THREE.PerspectiveCamera).fov = 35;
(camera as THREE.PerspectiveCamera).updateProjectionMatrix();
};
}, [camera]);
5. ボイス実装(VOICEVOX)
VOICEVOXで以下のボイスを作成・実装した。
FTG_VO_YOU_WIN → 勝利時
FTG_VO_YOU_LOSE → 敗北時
FTG_VO_FIGHT → FIGHT!コール時
FTG_VO_ROUND_01〜04 → ラウンドコール時
SE音量をハードコードしていた箇所を Assets.json から取得するように統一した。
const getSeVolume = (id: string, fallback = 1.0): number => {
const se = (assets as any).se?.find((s: any) => s.id === id);
return se?.volume ?? fallback;
};
audioController.playSe('FTG_VO_YOU_WIN', getSeVolume('FTG_VO_YOU_WIN'));
6. サウンドテスト
Assets.json に type フィールド(BGM / SE / VOICE)を追加し、タイトルメニューに SOUND TEST を追加した。
タイトルメニュー
├── TRAINING
├── SOUND TEST ← 新規追加
├── CREDIT
└── EXIT
FTGSoundTest.tsx として実装。タブ切り替え・リスト選択・再生トグルができる。Canvas外なので <Html> でラップした。
7. バトルUI刷新
頭上HPバー(Billboard追従)
HPバーをキャラクターの頭上に追従させる3D Billboardとして実装した。
const FTGHPBar: React.FC<...> = ({ physicsRef, hp, offsetY, color, label }) => {
const groupRef = useRef<THREE.Group>(null!);
useFrame(() => {
if (groupRef.current) {
groupRef.current.position.x = physicsRef.current.posX + offsetX;
}
});
return (
<group ref={groupRef} position={[0, offsetY, 0]} scale={[scale, scale, scale]}>
<Billboard>
{/* HPバー本体 */}
</Billboard>
</group>
);
};
Canvas外HUD(FTGBattleHUD)
他のモード(STG)の STGUI と同じパターンで実装した。
FTGHudStore.ts → グローバルref(ftgHudRef)
FTGTrainingScene → useFrameでftgHudRefに書き込み
FTGBattleHUD.tsx → setIntervalでftgHudRefを読んでDOM表示
WiredScene.tsx → Canvas外に <FTGBattleHUD /> を配置
// WiredScene.tsx(Canvas外)
{appMode === 'STG' && <STGUI />}
{appMode === 'FTG' && <FTGBattleHUD />} // ← 追加
表示内容:
- KO / YOU WIN / YOU LOSE / ROUND N / FIGHT!
- 右下:テンキーグリッド(入力可視化)+ ボタン表示 + FRAMEカウンター
- 左下:入力ログ(直近6件)
今後の課題
- VRコントローラー入力(Canvas統合が完了したのでいつでも着手可能)
- ステージ選択
- しゃがみキック技バリエーション
- コンボシステム