ブロック崩しの当たり判定の作り方|円と四角の判定・すり抜け対策・跳ね返る角度【JavaScript】
ブロック崩しを作るとき、一番むずかしいのが当たり判定です。「ボールがブロックに当たったか」「どの向きに跳ね返すか」を正しく決めないと、ボールがブロックにめり込んだり、すり抜けたりしてしまいます。
この記事では、僕が AI の Claude と一緒に作ったカカポの森のブロック崩しの当たり判定のしくみを、実際のコードで解説します。
まずは試してみよう:ボールの「すり抜け」
赤いボールが、緑の細い壁に向かって飛んでいきます。最初は「細かく分けて動かす:オフ」になっているので、ボールがときどき壁をすり抜けてしまいます。ボタンを押してオンにすると、すり抜けなくなります。
速さを下げると、オフのままでもすり抜けなくなります。なぜこうなるのかは、このあと説明します。
しくみ1:ゲームループと「経過時間」
ブロック崩しは、「動かす → 当たり判定 → 描く」を1秒に約60回くり返すことで動いています。このくり返しをゲームループと呼びます。
// ===== ゲームループ(毎秒およそ60回くり返す) =====
// requestAnimationFrame:画面を描き直すタイミングで、次の関数を呼んでくれる
let lastTime = performance.now();
function loop(now) {
// 前のコマからの経過時間(タブを離れていたときなどに大きくなりすぎないよう、上限を付ける)
const dt = Math.min((now - lastTime) / 1000, 1 / 30);
lastTime = now;
update(dt);
draw();
requestAnimationFrame(loop);
}
dt は「前のコマから何秒たったか」です。ボールの移動量に dt を掛けて計算しているので、画面の更新が速いパソコンでも遅いスマホでも、ボールの速さは同じになります。
しくみ2:円と四角の当たり判定
ボールは円、ブロックは四角です。「円と四角が重なっているか」は、四角の中で、円の中心に一番近い点を探すと判定できます。
// 円(ボール)と四角(ブロック)が重なっているか
function circleHitsRect(cx, cy, r, rect) {
const nearestX = Math.max(rect.x, Math.min(cx, rect.x + rect.w));
const nearestY = Math.max(rect.y, Math.min(cy, rect.y + rect.h));
const dx = cx - nearestX;
const dy = cy - nearestY;
return dx * dx + dy * dy < r * r;
}
Math.minとMath.maxで、円の中心の座標を四角の範囲内におさめる。これが「四角の中で一番近い点」になる- その点と円の中心の距離が、半径より短ければ重なっている
距離を比べるときは、平方根(Math.sqrt)を使わずに「距離の2乗」と「半径の2乗」を比べています。結果は同じで、計算が少し軽くなります。
しくみ3:ブロックの「どの面」に当たったか
当たったことがわかったら、次は跳ね返す向きを決めます。横の面に当たったら左右を、上下の面に当たったら上下を反転させます。
// ボールを、ブロックの「どの面」に当たったかで跳ね返す
function bounceOffBlock(b) {
const dx = ball.x - (b.x + b.w / 2);
const dy = ball.y - (b.y + b.h / 2);
const overlapX = b.w / 2 + ball.r - Math.abs(dx);
const overlapY = b.h / 2 + ball.r - Math.abs(dy);
if (overlapX < overlapY) {
// 横の面に当たった → 左右を反転
ball.vx = Math.sign(dx) * Math.abs(ball.vx);
ball.x += Math.sign(dx) * overlapX;
} else {
// 上下の面に当たった → 上下を反転
ball.vy = Math.sign(dy) * Math.abs(ball.vy);
ball.y += Math.sign(dy) * overlapY;
}
}
どの面に当たったかは、ボールとブロックが、横と縦にどれだけ重なっているかで判断しています。重なりが小さいほうの面に当たった、と考えるわけです。
跳ね返すときに、重なっている分だけボールを外に押し出しているのもポイントです。これがないと、ボールがブロックにめり込んだまま何度も跳ね返ってしまいます。
しくみ4:すり抜け対策は「細かく分けて動かす」
デモで見たすり抜けの原因は、ボールが1コマで動く距離が、壁の厚さより長くなることです。1コマ前は壁の手前、次のコマではもう壁の向こう。重なっている瞬間がないので、当たり判定に引っかかりません。
そこで、1コマの移動を細かく分けて、少し進むたびに当たり判定をしています。
// ボールを動かす。速いと1コマでブロックをすり抜けてしまうので、細かく分けて動かす
function moveBall(dt) {
const distance = ball.speed * dt;
const steps = Math.ceil(distance / (ball.r * 0.8));
for (let i = 0; i < steps && state === "playing"; i++) {
ball.x += (ball.vx * dt) / steps;
ball.y += (ball.vy * dt) / steps;
// …(壁・パドル・ブロックとの当たり判定。下で説明)…
}
}
1回に進む距離が「ボールの半径の0.8倍」以下になるように分ける回数を決めています。これなら、どんなに速くても、ボールが壁を飛び越えることはありません。
ブロックに当たったときは、1回に1つのブロックだけを処理しています。
// ふつう:1回に当たるのは1つだけ。当たったら跳ね返る
for (const b of blocks) {
if (b.hp > 0 && circleHitsRect(ball.x, ball.y, ball.r, b)) {
bounceOffBlock(b);
ballHitsBlock(b);
break;
}
}
}
しくみ5:パドルに当たった位置で角度を変える
ブロック崩しを面白くするのが、パドルのどこで打ち返したかで、ボールの角度が変わるしくみです。
// カカポ(パドル):当たった位置で、跳ね返る角度を変える
const onPaddle =
ball.vy > 0 &&
ball.y + ball.r >= paddle.y &&
ball.y < paddle.y + paddle.h &&
Math.abs(ball.x - paddle.x) <= paddle.w / 2 + ball.r;
if (onPaddle) {
// 真ん中なら真上、はしっこほどななめ(最大60度)
const hitPos = Math.max(-1, Math.min(1, (ball.x - paddle.x) / (paddle.w / 2)));
const angle = hitPos * (Math.PI / 3);
// 打ち返すたびに少しずつ速くなる(最大で1.25倍)
ball.speed = Math.min(ball.speed * 1.01, STAGES[stageNum].speed * 1.25);
ball.vx = ball.speed * Math.sin(angle);
ball.vy = -ball.speed * Math.cos(angle);
ball.y = paddle.y - ball.r;
sound.paddle();
}
hitPosは、パドルの真ん中で 0、左はしで -1、右はしで 1 になる値- それに60度(
Math.PI / 3)を掛けて、跳ね返る角度にする - 角度から、
sinで横の速さ、cosで縦の速さを計算する
これで「真ん中なら真上、はしっこほどななめ」に跳ね返ります。打ち返すたびに少しずつ速くなるので、だんだん難しくなっていきます。
しくみを知っておくと、修正がこわくなくなる
この当たり判定も、AIの Claude に相談しながら作りました。AIを使えば動くものはすぐにできますが、「跳ね返り方を少し変えたい」と思ったときに、どこをさわればいいかわからないと手が止まってしまいます。
当たり判定は「重なっているか調べる」「どの面か決める」「押し出して跳ね返す」の3つに分かれている、とわかっていれば、修正したい部分だけを狙って直せます。
AIと一緒にゲームを作った体験はこちらの記事に書いています。ゲーム作りが初めてなら、クイズゲームの作り方から始めるのもおすすめです。
まとめ
- 円と四角の当たり判定は、「四角の中で、円の中心に一番近い点」との距離で調べる
- どの面に当たったかは、横と縦の重なりの小さいほうで決める。跳ね返すときは押し出しも忘れずに
- 速いボールのすり抜けは、1コマの移動を細かく分けて判定すれば防げる
- パドルに当たった位置で角度を変えると、ぐっとゲームらしくなる
コードの全体は GitHub で公開しています。


