[Astro #88] LAYER FIGHTER 開発2日目 ─ ヒットエフェクト・コマンド技・自作アニメーションの追加

[Astro #88] LAYER FIGHTER 開発2日目 ─ ヒットエフェクト・コマンド技・自作アニメーションの追加

はじめに

前回の記事 では、格闘ゲーム「LAYER FIGHTER」のトレーニングモードの骨格を実装しました。

VRMキャラの移動・ジャンプ・パンチ・キック、ヒットボックスの可視化、敵キャラの配置とヒット判定まで。

今回はその続きで、「殴った時の手触り」を徹底的に作り込む1日でした。格闘ゲームの面白さは「ボタンを押した瞬間のフィードバック」にあるので、演出系を先に仕上げてからゲーム性の拡張に進みます。

[Astro #88] LAYER FIGHTER 開発2日目 ─ ヒットエフェクト・コマンド技・自作アニメーションの追加 [Astro #88] LAYER FIGHTER 開発2日目 ─ ヒットエフェクト・コマンド技・自作アニメーションの追加

動画:

ヒットストップの実装

格闘ゲームの「重さ」の正体はヒットストップです。攻撃が当たった瞬間に、2〜4フレームだけゲーム全体を止める演出で、これがあるとないとでは手触りが段違いに変わります。

実装はゲームループの先頭にカウンターを入れるだけです。

const hitStopRef = useRef(0);

// ゲームループの先頭
if (hitStopRef.current > 0) {
  hitStopRef.current--;
  continue; // このフレームは全ロジックをスキップ
}

// ヒット時
hitStopRef.current = move.hitStop ?? 0;

パンチは2フレーム、キックは4フレームのヒットストップを設定しました。この数値もMoves.jsonに hitStop として外部化しているので、技ごとに微調整が可能です。

ヒットパーティクル

最初は白フラッシュ(画面全体を一瞬白くする)を実装しましたが、あまりに味気なかったので、衝突地点にパーティクルを飛ばす方式に変更しました。

const HitParticle: React.FC<{ x: number; y: number; scale?: number }> = ({ x, y, scale = 0.5 }) => {
  // 12個の粒子を生成、それぞれランダムな方向に飛ばす
  const particlesRef = useRef(
    Array.from({ length: 12 }, () => ({
      vx: (Math.random() - 0.5) * 0.15,
      vy: (Math.random() - 0.3) * 0.12,
      life: 1.0,
    }))
  );
  // ...
};

パーティクルのサイズも技ごとに変えています。パンチは hitParticleScale: 0.02 で控えめな火花、キックは 0.08 で大きめのエフェクト。弱攻撃は「パチッ」、強攻撃は「バシッ」という視覚的な差が生まれます。

Moves.jsonの拡張

ここまでで技パラメーターのJSON定義がかなり充実してきました。1つの技に対して制御できる項目は以下の通りです。

{
  "comment":   "キック(中)",
  "id":        "KICK_M",
  "hitSe":     "FTG_SE_HIT_MIDDLE",
  "hitStop":    4,
  "hitParticleScale": 0.08,
  "hitSeVolume": 0.8,
  "animSpeed":  1.3,
  "state":     "KICKING",
  "animId":    "SYS_KICK",
  "strength":  "medium",
  "startup":   38,
  "active":    5,
  "recovery":  40,
  "hitstun":   20,
  "knockback": 0.8,
  "damage":    10,
  "hitboxes": [
    { "comment": "すね", "offsetX": 0.8, "offsetY": 0.9, "width": 0.3, "height": 0.2, "depth": 0.2 },
    { "comment": "足先", "offsetX": 1.3, "offsetY": 1.3, "width": 0.4, "height": 0.3, "depth": 0.3 }
  ]
}

フレームデータ(startup/active/recovery)、演出(hitStop/hitParticleScale/hitSe/hitSeVolume)、アニメーション速度(animSpeed)、ヒットボックスの位置とサイズまで、全部JSONで管理できるようになりました。コードを一切触らずに技の調整ができます。

HPバー

左上にテキストで表示していた ENEMY HP: 100 を、画面上部のバーに変更しました。格闘ゲームでお馴染みの対面配置です。

HP30%以下になるとバーの色が赤く変化するようにしていて、「あとひと押し」の緊張感が出ます。

<div style={{
  width: `${enemyHp}%`, height: '100%',
  background: `linear-gradient(90deg,
    ${enemyHp > 30 ? '#ff4444' : '#ff0000'},
    ${enemyHp > 30 ? '#ff8844' : '#ff2222'})`,
  transition: 'width 0.15s ease-out',
}} />

ガードの実装

格闘ゲームの読み合いの基本となるガードを実装しました。□ボタン(キーボードC)でガードポーズを取ります。

立ちガードとしゃがみガードの2種類を用意していて、↓+□で自動的にしゃがみガードに切り替わります。

if (buttons.includes('G') && (newDpad === '2' || newDpad === '1' || newDpad === '3')) {
  transitionState('CROUCH_GUARDING', 1, frame);
} else if (buttons.includes('G')) {
  transitionState('GUARDING', 1, frame);
}

現時点ではプレイヤー側のガードポーズだけで、敵のガード処理(ダメージ軽減やノックバック低減)は将来のCPU AI実装時に入れる予定です。

コマンド入力技

方向入力+ボタンの組み合わせで異なる技が出るシステムを実装しました。

コマンド説明
K回し蹴りニュートラルキック
6+K膝蹴り前入力+キック
3+K前蹴り斜め下+キック
2+K下蹴り下入力+キック
2+Pしゃがみパンチ下入力+パンチ

ここでハマったのが6+Kの同時押し問題でした。テンキーの3や2は↓を押しっぱなしにしながらボタンを押せるので同時押しが自然にできますが、6(→)+Kは→を押しながらKを押すと、→を離すタイミングによっては通常キックが出てしまいます。

これを解決するために入力バッファを実装しました。直近10フレーム以内の方向入力を記憶して、ボタンが押された瞬間にバッファを参照する仕組みです。

const inputBufferRef = useRef<{ dpad: DPad; frame: number }[]>([]);

// 毎フレーム記録
inputBufferRef.current.push({ dpad: newDpad, frame });
while (inputBufferRef.current.length > 0 && frame - inputBufferRef.current[0].frame > 10) {
  inputBufferRef.current.shift();
}

// バッファ判定
const wasPressed = (dpad: DPad): boolean => {
  return inputBufferRef.current.some(b => b.dpad === dpad);
};

さらに rawRight(→キーが今押されているか)も直接チェックすることで、「→押しっぱなし+K」でも「→→K」のような素早い入力でも膝蹴りが出るようになりました。

ちなみに3+K(↓→)で膝蹴りが出てしまうバグもありました。rawRighttrue になるので膝蹴り判定に引っかかっていたのですが、rawRight && !pressDown にすることで「右だけ押してる時」と「右+下」を正しく分離できました。この辺のif文の優先順序は本当に微妙で、格ゲーの入力処理が難しいと言われる理由がよくわかりました。

2キャラ追従カメラ

敵を攻撃してノックバックさせると画面外に消えてしまう問題がありました。カメラが自キャラのX座標だけを追従していたためです。

これを「2キャラの中間点を追従、距離に応じてズーム」する方式に変更しました。

useFrame(() => {
  const px = physicsRef.current.posX;
  const ex = enemyPhysicsRef.current.posX;

  // 2キャラの中間点
  const midX = (px + ex) / 2;
  // 距離に応じてカメラを引く
  const dist = Math.abs(px - ex);
  const targetZ = Math.max(4, dist * 1.2 + 2);

  camera.position.x = THREE.MathUtils.lerp(camera.position.x, midX, 0.08);
  camera.position.z = THREE.MathUtils.lerp(camera.position.z, targetZ, 0.06);
  camera.lookAt(midX, 1.2, 0);
});

これで離れると引き・近づくと寄りの自然なカメラワークになり、両キャラが常に画面内に収まります。最初からやるべきだった変更ですが、敵キャラを配置してノックバックが動くようになるまで気づけない類の問題でした。

VRM Posing Desktop で自作アニメーション

Mixamoのフリーアニメーションだけでは賄えない技が出てきたので、Steamで1000円で販売されている「VRM Posing Desktop」を導入しました。

VRMモデルを読み込んで、ボーンを直感的に動かしてポーズを作り、.vrma 形式で直接エクスポートできるツールです。Mixamoと違ってFBXからの変換が不要なので、作ったアニメーションをそのままプロジェクトに投入できます。

このツールで最初に作ったのは「しゃがみガード」のポーズでした。しゃがんだ状態で両腕を前に構える1フレームのポーズ。VRM Posing Desktop上でしゃがみポーズを選択して、腕の位置をドラッグで調整するだけ。5分で完成して、即座にゲームに反映できました。

続けて「しゃがみパンチ」も作りました。こちらは1ポーズだけだとthree.jsが前のアニメーションから自動補間してしまい、動きが不自然になりました。対策として animSpeed: 2.5 を設定して補間を一瞬で終わらせつつ、将来的にはVRM Posing Desktopで2コマ(構え→パンチ)のアニメーションに差し替える予定です。

効果音の強度別管理

攻撃の強さに応じてヒットSEを変えられるようにしました。

Assets.jsonに弱・中・強の3種類のヒットSEを定義しておき、Moves.jsonの各技から hitSehitSeVolume で参照する構造です。パンチは軽い打撃音、キックは重めの打撃音、将来の強攻撃にはさらに重い音を割り当てます。

"hitSe":       "FTG_SE_HIT_MIDDLE",
"hitSeVolume":  0.8

効果音は 効果音ラボ さんからお借りしています。打撃音が3段階あるので格ゲーにぴったりでした。

AudioController のBGMバグ修正

タイトル画面でBGMが再生されない問題がありました。原因は THREE.Audio(BGM用)がCanvas内のカメラにアタッチされた時にしか生成されず、タイトル画面にはCanvasがないため bgmAudionull のまま playBgm が空振りしていたことでした。

コンストラクタで bgmAudio を即座に生成するように修正して解決しました。タイトル画面にBGMが流れるだけで雰囲気が一気に変わります。

まとめ

今日1日で追加した機能をまとめます。

カテゴリ機能管理先
演出ヒットストップMoves.json hitStop
演出ヒットパーティクルMoves.json hitParticleScale
演出HPバーコード
ゲーム性立ちガード / しゃがみガードFighterState
ゲーム性コマンド技(6K/3K/2K/2P)Moves.json + 入力バッファ
技術入力バッファ(10フレーム)コード
技術2キャラ追従カメラコード
技術技別ヒットSEMoves.json hitSe / hitSeVolume
技術アニメ速度のJSON化Moves.json animSpeed
ツールVRM Posing Desktop導入自作 .vrma

Moves.jsonがゲームデザイナーの作業台になっていて、コードを触らずに技の「手触り」を調整できる状態になりました。

次回以降

トレーニングモードの完成度がかなり上がったので、そろそろゲームとして遊べる形にしたいところです。

  • ラウンド制(HP0で1本、2本先取で勝利、勝利BGM)
  • 被弾のけぞりアニメーション(VRM Posing Desktopで自作)
  • 簡易CPU AI(接近→攻撃→離脱のパターン)
  • ステージモデルの読み込み(Assets.jsonにすでに2ステージ定義済み)

VRM Posing Desktopの導入で自作アニメーションのハードルが大幅に下がったので、モーションの種類を増やしながら進めていきます。