JavaScriptの当たり判定入門:四角・円・ピクセルの3種類
遊びをつくる執筆にClaudeを使用しています。

ブラウザゲームを作るとき、「なぜここで当たるのか」「なぜすり抜けるのか」という問題は避けて通れません。当たり判定はゲームの爽快感や公平感に直結するため、適切な方式を選ぶことが完成度を大きく左右します。 JavaScriptで使われる当たり判定の方式は主に三つです。四角形同士の重なりを調べる矩形判定(AABB)、円同士の距離を調べる円判定、そして画像の不透明ピクセルを照合するピクセル判定です。この三つはそれぞれ計算コスト・精度・実装の手間がまったく異なります。「どの方式をどの場面に使うか」という判断基準を持つことが、当たり判定の実装で最初に必要なことです。 この記事では三つの方式を順番に実装方法とともに説明し、組み合わせて使うときの考え方まで解説します。
矩形判定(AABB):最初に覚える基本形
AABB(Axis-Aligned Bounding Box) とは、軸に平行な四角形が重なっているかどうかを調べる方法です。回転のない長方形の当たり判定として最も広く使われています。
判定の考え方はシンプルです。二つの矩形が重なっていない条件を先に列挙し、その否定を取ります。矩形Aの右端がBの左端より左にある、Aの左端がBの右端より右にある、Aの下端がBの上端より上にある、Aの上端がBの下端より下にある——このいずれかが成立すれば重なっていません。
function rectsOverlap(a, b) {
return (
a.x < b.x + b.w &&
a.x + a.w > b.x &&
a.y < b.y + b.h &&
a.y + a.h > b.y
);
}引数の各オブジェクトは { x, y, w, h } の形で、x と y は左上角の座標、w と h は幅と高さを表します。この関数は真偽値を返すので、if (rectsOverlap(player, enemy)) のように直接条件分岐に使えます。
矩形判定が向いている場面はプラットフォーマーの地面判定、UIボタンのクリック判定、タイルマップ上のコリジョンなどです。計算量が非常に少ないため、フレームごとに多数の組み合わせを確認するゲームでも問題なく動きます。
失敗しやすい点は座標系の混乱です。Canvasの drawImage では原点が左上角ですが、ゲームロジックによっては中心座標で管理している場合があります。x が中心を指しているのかどうかを実装の最初に決めておくと、ズレを防げます。もう一つ注意が必要なのは、矩形は見た目の形と一致しないことです。斜めのオブジェクトや星形のスプライトに矩形を当てると、見た目では当たっていないのに判定が発生する「ゴースト判定」が起きます。
円判定:回転に強く、シンプルに精度を上げる
円判定は二つの円の中心間距離を計算し、それが両方の半径の合計より小さければ重なっていると判断します。
function circlesOverlap(a, b) {
const dx = a.cx - b.cx;
const dy = a.cy - b.cy;
const distSq = dx * dx + dy * dy;
const rSum = a.r + b.r;
return distSq < rSum * rSum;
}引数は { cx, cy, r } の形で、cx と cy が中心座標、r が半径です。Math.sqrt を使うと距離の平方根を毎フレーム計算することになり、パフォーマンスに影響します。そのため距離の二乗(distSq)と半径の合計の二乗(rSum * rSum)を比較するのが定番のやり方です。
円判定が向いている場面は弾丸と敵キャラクター、コインや爆発エフェクトとの衝突など、丸みのあるオブジェクトが多いゲームです。回転しても半径は変わらないため、スプライトを回転させるゲームでは矩形より精度が高くなります。
矩形と円を組み合わせる場面も多くあります。たとえばプレイヤーの当たり判定は円で管理しつつ、壁や床は矩形で表すケースです。このとき「円と矩形の重なり」を別途判定する必要があります。考え方は、矩形内で円の中心に最も近い点(クランプ点)を求め、そこから中心までの距離が半径より小さければ衝突、というものです。
function circleRectOverlap(circle, rect) {
const nearestX = Math.max(rect.x, Math.min(circle.cx, rect.x + rect.w));
const nearestY = Math.max(rect.y, Math.min(circle.cy, rect.y + rect.h));
const dx = circle.cx - nearestX;
const dy = circle.cy - nearestY;
return dx * dx + dy * dy < circle.r * circle.r;
}ピクセル判定:精度は最高、コストも最高
ピクセル判定は、二つの画像の不透明ピクセルが空間的に重なっているかどうかをピクセル単位で確認します。「見た目通りの当たり」が必要なパズルゲームや、複雑なシルエットを持つオブジェクトが登場するゲームで有効です。
仕組みとしては、オフスクリーンの <canvas> に片方のスプライトを描画し、getImageData でピクセル配列を取得します。もう一方についても同様に取得し、二つのバウンディングボックスが重なる領域内で双方のアルファ値(透明度)がゼロより大きいピクセルが存在するかどうかを確認します。
実装の骨子は次のようになります。
- 二つのスプライトの矩形領域が重なるかを先にAABBで確認する(重ならなければここで終了)。
- 重なる矩形領域(交差領域)の座標を計算する。
- 交差領域の各ピクセルについて、スプライトAとスプライトBのそれぞれのピクセルデータを参照し、両方のアルファが閾値(一般的には0より大きければ十分)を超えていれば衝突とみなす。
ピクセル判定の注意点はいくつかあります。まず、getImageData はCanvas 2D APIを使うため、画像が同一オリジン(Same-Origin)であるか、CORS対応していることが必要です。外部ドメインから取得した画像をピクセル解析しようとすると、セキュリティエラーで弾かれます。次に、このアプローチは毎フレーム大量のループを走らせるため、スプライトが大きかったり同時に判定するオブジェクトが多かったりすると、フレームレートが落ちます。実際のゲームでは最初にAABBで大まかに絞り込み、重なりが検出されたペアだけをピクセル判定にかける「二段階判定」が一般的なパターンです。
ピクセル判定をゲームの全オブジェクトに適用しようとするのは過剰設計になりやすいです。「このゲームで本当にピクセル精度が必要な場面はどこか」を先に絞り込んでから導入するのが現実的です。
三種類を組み合わせる設計の考え方
実際のゲームでは、一種類の判定だけで完結することはほとんどありません。判定方式の選択は「精度が必要かどうか」と「どれだけ頻繁に計算するか」のトレードオフで決まります。
判断の基本的な流れは次のとおりです。
- オブジェクト同士が明らかに遠い段階:何もしない(空間分割などで対象を絞る)
- 大まかな重なりチェック:AABB(ほぼゼロに近いコスト)
- 形状が丸い、または回転する:円判定に切り替える
- シルエットの精度が重要:ピクセル判定を最後の段階にだけ使う
たとえばシューティングゲームであれば、弾と敵の判定は円で十分なことが多く、ステージの壁はAABBで問題ありません。ジグソーパズル風のゲームでピースがはまったかどうかを判断するなら、ピクセル判定が適切です。
もう一つ重要なのは、判定の「基準点」を統一することです。AABBは左上角基準、円は中心基準が自然ですが、ゲームロジック全体で座標の意味が混在すると、判定のズレが見つけにくいバグになります。プレイヤーオブジェクトを設計する段階で「このゲームでは中心座標で管理する」などのルールを決め、それに合わせて判定関数を揃えておくと、後からの修正が楽になります。
次の行動
三種類の判定関数はそれぞれ単独のHTMLファイルに貼り付けてすぐ動作確認ができます。まずAABBだけを使ったシンプルなサンプルを手元で作り、オブジェクトを動かしながら判定が正しく発火するかを目で確認するところから始めてみてください。動くものができたら、アソバースにHTMLファイルとして投稿し、他のプレイヤーに試してもらうことで精度の問題に気づきやすくなります。

