[Astro #90] LAYER FIGHTER // バトル強化 - 被弾アニメ・プレイヤーKO・敵攻撃バリエーション・2本先取
はじめに
前回(Astro #89)でタイトル画面を完成させた。今回はバトルシステムの強化に集中した。
[Astro #89] LAYER FIGHTER // タイトル・バトル・クレジット実装 // PROTOCOL.LAIN
React Three Fiber + VRM で格ゲー「LAYER FIGHTER」を大幅強化。無限スクロールタイトル画面(VRMアバター・猫・ゴールハウス演出・ループ)、敵AIのパンチ攻撃とダメージ判定(EnemyPhysics拡張)、ラウンド推移(KO→YOU WIN→ROUND N→FIGHT)、GLBステージ読み込み、Assets.json駆動のクレジットスライダーまで実装した記録。
lain-lab.com
1. 被弾アニメ(SYS_HURT)
Assets.json に追加
{
"id": "SYS_HURT",
"type": "system",
"name": "Hurt",
"file": "/animations/anim/Reaction.vrma",
"version": "1.0.0",
"animSpeed": 0.8,
"duration": 40
}
duration でアニメ再生フレーム数を管理する。animSpeed で再生速度も調整できる。
ANIM_MAP に追加
HITSTUN: 'SYS_HURT',
NO_LOOP_ANIMS に追加
const NO_LOOP_ANIMS = new Set([
'SYS_PUNCH', 'SYS_CROUCH_PUNCH', 'SYS_KICK',
'SYS_KICK_KNEE', 'SYS_KICK_FORWARD', 'SYS_KICK_LOW',
'SYS_HURT', // ← 追加
]);
2. HITSTUN 中の入力キャンセル防止
被弾中に移動ボタンを押すとアニメがキャンセルされる問題を修正した。
ステートマシンの入り口で HITSTUN 中は入力を受け付けず、actionDone で IDLE に戻すだけにする:
const isLocked = ['PUNCHING', 'KICKING', 'HITSTUN', ...].includes(curState);
const actionDone = actionFrameRef.current >= actionDurRef.current;
if (!isLocked || actionDone) {
if (curState === 'HITSTUN' && actionDone) {
transitionState('IDLE', 1, frame);
} else {
// 通常の入力処理
}
}
ヒット時の transitionState に duration を渡すことで、アニメが終わるまで入力をブロックできる:
const hurtDef = (assets as any).animations?.find((a: any) => a.id === 'SYS_HURT');
transitionState('HITSTUN', hurtDef?.duration ?? 40, frame);
3. 敵の被弾アニメ(enemyHitstunAnimRef)
敵にはプレイヤーと同じステートマシンがないため、enemyHitstunAnimRef で被弾アニメの残りフレームを管理する暫定対処を実装した。
const enemyHitstunAnimRef = useRef(0);
// ヒット時
setEnemyAnimId('SYS_HURT');
const hurtDef = (assets as any).animations?.find((a: any) => a.id === 'SYS_HURT');
enemyHitstunAnimRef.current = hurtDef?.duration ?? 40;
// AIブロック内
if (enemyHitstunAnimRef.current > 0) {
enemyHitstunAnimRef.current--;
ep.facingRight = p.posX > ep.posX; // 向きだけ更新
} else {
// 通常のAI処理
}
根本的には敵にもステートマシンを持たせるべきだが、現状は暫定対処で動いている。
4. プレイヤーKO判定
敵のKO判定しかなかったので、プレイヤー側のKO判定を追加した。
koWinnerRef で勝者を管理し、表示用に koWinner state と同期する:
const koWinnerRef = useRef<'PLAYER' | 'ENEMY'>('PLAYER');
const [koWinner, setKoWinner] = useState<'PLAYER' | 'ENEMY'>('PLAYER');
// 敵KO
if (roundPhaseRef.current === 'FIGHTING' && ep.hp <= 0) {
koWinnerRef.current = 'PLAYER';
setKoWinner('PLAYER');
roundPhaseRef.current = 'KO';
setRoundPhase('KO');
// ...
}
// プレイヤーKO
if (roundPhaseRef.current === 'FIGHTING' && p.hp <= 0) {
koWinnerRef.current = 'ENEMY';
setKoWinner('ENEMY');
roundPhaseRef.current = 'KO';
setRoundPhase('KO');
// ...
}
WAIT_INPUT / VICTORY の表示で koWinner で分岐:
{koWinner === 'PLAYER' ? 'YOU WIN' : 'YOU LOSE'}
5. 敵攻撃バリエーション(Assets.json 駆動)
これまで PUNCH_L だけをハードコードしていた敵の攻撃を、Assets.json で管理する形に変更した。
{
"id": "ENEMY_CHEN_QIANYU",
"attackMoves": ["PUNCH_L", "KICK_M"]
}
コード側はランダム選択するだけ:
const attackMoves = enemyVrmDef?.attackMoves ?? ['PUNCH_L'];
const selectedMoveId = attackMoves[Math.floor(Math.random() * attackMoves.length)];
ep.attackMoveId = selectedMoveId;
// アニメも Moves.json の animId から取得
const selectedMove = (moves as any).moves.find((m: any) => m.id === selectedMoveId);
setEnemyAnimId(selectedMove?.animId ?? 'SYS_PUNCH');
JSON に技を追加するだけで敵の攻撃バリエーションが増やせる。
6. 2本先取の勝利条件
playerWinsRef / enemyWinsRef を追加してゲームループ内で参照できるようにした:
const playerWinsRef = useRef(0);
const enemyWinsRef = useRef(0);
// 勝利時に ref も更新
playerWinsRef.current++;
setPlayerWins(prev => prev + 1);
WAIT_INPUT フェーズで2本先取チェック:
if (playerWinsRef.current >= 2 || enemyWinsRef.current >= 2) {
// ゲーム終了 → 全リセット
playerWinsRef.current = 0;
enemyWinsRef.current = 0;
setPlayerWins(0);
setEnemyWins(0);
// キャラクターもリセット
// ...
}
今後の課題
- しゃがみ中のヒットボックス判定(しゃがみで上段攻撃を避けられるようにする)
- しゃがみ状態でのキック技バリエーション
- 敵のステートマシン化(被弾アニメの暫定対処を根本解決)
- コンボシステム(難易度高いので後回し)