マガジン

ゲームの当たり判定がすり抜ける:速度が大きいときの対処

遊びをつくる

執筆にClaudeを使用しています。

同じ目のサイコロと操作の跡が二本の平行な小道に並ぶ、再現手順の模型。
記事のテーマをもとに制作したAI生成イラスト。

弾が壁をすり抜けた、高速で動く敵がプレイヤーをすり抜けた——ブラウザゲームを作り始めると、必ずといっていいほど一度は経験するバグです。原因はコードのミスではなく、当たり判定の仕組みそのものが持つ「速度が上がるほど見落としが増える」という性質にあります。 このすり抜けは「トンネリング(tunneling)」と呼ばれます。毎フレームの処理でオブジェクトの位置をΔxだけ動かしたとき、そのΔxが壁の厚みより大きければ、オブジェクトは壁の手前にいた状態から一気に壁の向こう側へワープします。その瞬間、重なり判定は一度も真にならないまま通過が完了してしまいます。速度を上げる・フレームレートが落ちる・オブジェクトを小さくする、この三つのどれかが起きるとトンネリングが発現しやすくなります。 この記事では、トンネリングの原因を確認したあと、実装の手間と精度のバランスから選べる三つの対処法を順に紹介します。自分のゲームの状況に合わせて選んでください。

この記事の目次
  1. なぜ速度が上がると当たり判定が壊れるのか
  2. 対処法1:移動量の上限を設ける(最も手軽)
  3. 対処法2:連続当たり判定(レイキャスト・ステップ分割)
  4. 対処法3:「めり込み後に押し戻す」方式との使い分け
  5. デバッグで確認すべき三つのポイント
  6. 次の行動
  7. 参考資料

なぜ速度が上がると当たり判定が壊れるのか

多くのブラウザゲームはフレームごとに「位置を更新 → 当たり判定 → 描画」のループを回しています。JavaScriptで言えば requestAnimationFrame の中で x += vx をしてから矩形同士の重なりを調べる、という流れです。

問題は、この判定が「現時刻の状態のスナップショット」しか見ないことです。フレーム間の軌跡は無視されます。弾の速度が毎フレーム20pxで、壁の厚みが10pxなら、弾は壁の領域を一度も踏まずに反対側へ出られます。フレームレートが60fpsから30fpsに下がれば1フレームで動く距離は倍になり、それだけトンネリングが起きやすくなります。

「オブジェクトを大きくすれば解決する」と思うかもしれませんが、当たり判定の矩形を広げると今度は見た目と合わなくなります。根本的には「移動軌跡を考慮した判定」か「1フレームの移動量を制限する」かのどちらかで対処します。

対処法1:移動量の上限を設ける(最も手軽)

トンネリングが起きるのは「1フレームの移動量 > オブジェクトが通り抜けてはいけない最小サイズ」のときです。ならば、移動量をそのサイズ以下に抑えれば物理的にすり抜けません。

vx = Math.max(-MAX_SPEED, Math.min(MAX_SPEED, vx));

MAX_SPEED に設定すべき値は、「ゲーム内で最も薄い壁や判定領域の幅の半分以下」が目安です。壁の最小幅が16pxなら MAX_SPEED を8px以下にします。

この方法はシンプルですが、ゲームデザインに制約が生まれます。弾幕ゲームで弾を高速にしたい、爆発のノックバックを大きくしたい、という場面では速度の上限が演出の邪魔をすることがあります。速度を見せたい演出より、確実な衝突判定を優先する場面——たとえばパズルやプラットフォーマーの足場判定——に向いています。

対処法2:連続当たり判定(レイキャスト・ステップ分割)

速度制限でデザインが壊れる場合は、フレーム間の軌跡そのものを判定に使います。代表的な手法が「ステップ分割」と「レイキャスト」です。

ステップ分割

1フレームの移動を複数の小ステップに分けて、ステップごとに判定を行います。

const steps = Math.ceil(speed / MIN_SIZE);
const stepX = vx / steps;
const stepY = vy / steps;

for (let i = 0; i < steps; i++) {
  x += stepX;
  y += stepY;
  if (checkCollision()) {
    resolveCollision();
    break;
  }
}

steps の計算に使う MIN_SIZE は、先ほどと同じく「最も薄い判定領域の幅の半分」です。速度が大きくなるほどステップ数が増えるので、処理コストが比例して上がる点に注意してください。オブジェクト数が少ない場面では十分実用的ですが、弾が100発以上飛ぶような弾幕系では重くなることがあります。

レイキャスト

矩形ではなく「移動前の位置から移動後の位置への線分」と「障害物」との交差判定を行う方法です。当たった位置を正確に求められるため、反射角の計算もしやすくなります。ただし実装が複雑になります。矩形同士のレイキャストは「ミンコフスキー和(障害物矩形を当たり判定サイズ分だけ拡張してから線分と判定する)」を使うと整理しやすくなります。

すでにPhaser.jsやMatter.jsといった物理エンジンを使っている場合は、設定で連続当たり判定(CCD:Continuous Collision Detection)を有効化するオプションがあることが多いため、まずドキュメントを確認するのが最短です。

対処法3:「めり込み後に押し戻す」方式との使い分け

一般的なゲームエンジンの多くは、毎フレーム「めり込みを検出したら押し戻す」方式(離散衝突 + 位置補正)を基本にしています。これはプレイヤーが壁を歩く・乗る場面には十分機能しますが、弾や高速オブジェクトには向きません。

判断の目安をまとめると次のようになります。

  • プレイヤー・敵(中低速)、推奨する対処:めり込み後押し戻しで十分
  • 弾・発射物(高速・小サイズ)、推奨する対処:ステップ分割またはレイキャスト
  • すべてのオブジェクト、推奨する対処:速度の上限を設けることで保険をかける

速度制限とステップ分割は排他的ではありません。速度の上限を「常識的な範囲」に設けたうえで、それでも超えた場合にステップ分割が走る設計にしておくと、パフォーマンスを安定させながら安全網を二重にできます。

デバッグで確認すべき三つのポイント

トンネリングが起きているかどうかを確かめるとき、次の三点を順番に調べると原因を絞りやすくなります。

  1. フレームレートを意図的に下げて再現するか確認する。requestAnimationFrame のコールバックで deltaTime を大きくシミュレートし、すり抜けが再現されれば速度起因のトンネリングです。
  2. 当たり判定の矩形を可視化する。ゲームの描画ループに判定矩形を半透明で重ねて描き、見た目とずれていないかを目視します。ずれがあれば座標系やスケールの設定ミスが原因です。
  3. 移動量を console.log で毎フレーム出力し、壁の厚みと比較する。移動量が壁の厚みを超えているフレームがあれば、対処法1か2を適用します。

可視化のためだけにデバッグ用のフラグを用意し、リリース前にオフにするのが実務上のやり方です。コードに残しておくと次回のデバッグでも使えます。

次の行動

まず自分のゲームで「最も薄い当たり判定領域の幅」と「最大移動速度(px/frame)」を数値で確認してください。移動速度がその幅を超えていれば、速度の上限を設けるかステップ分割を導入することで大半のトンネリングは解消できます。対処を加えたゲームはアソバースにZIPまたはHTMLで投稿して、実際のブラウザ環境で動作を確かめてみてください。

参考資料

  • MDN Web Docs「requestAnimationFrame」
  • Phaser.js 公式ドキュメント(Arcade Physics / CCD の設定)
  • Matter.js 公式ドキュメント(Bodies オプション)