Collision Detection
Collision detection finds when game objects overlap; collision response decides what happens (bounce, stop, damage). Most engines bundle physics but you should know the primitives: AABB, circle, raycast, SAT.
Shapes, broadphase, narrowphase, response
EXAMPLE
// 1) Axis-Aligned Bounding Box (AABB) — simplest, fast
function aabb(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;
}
const player = { x: 10, y: 20, w: 32, h: 48 };
const wall = { x: 30, y: 0, w: 16, h: 96 };
if (aabb(player, wall)) console.log('hit!');
// 2) Circle-circle
function circles(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
const r = a.r + b.r;
return dx * dx + dy * dy < r * r; // compare squared distance (avoid sqrt)
}
// 3) Circle vs AABB — clamp + distance
function circleAabb(circle, box) {
const cx = Math.max(box.x, Math.min(circle.x, box.x + box.w));
const cy = Math.max(box.y, Math.min(circle.y, box.y + box.h));
const dx = circle.x - cx;
const dy = circle.y - cy;
return dx * dx + dy * dy < circle.r * circle.r;
}
// 4) Point-in-AABB / point-in-circle
function pointInBox(p, box) {
return p.x >= box.x && p.x <= box.x + box.w && p.y >= box.y && p.y <= box.y + box.h;
}
function pointInCircle(p, c) {
const dx = p.x - c.x;
const dy = p.y - c.y;
return dx * dx + dy * dy < c.r * c.r;
}
// 5) Raycast — line segment vs AABB (slab method)
function rayAabb(ray, box) {
const tx1 = (box.x - ray.x) / ray.dx;
const tx2 = (box.x + box.w - ray.x) / ray.dx;
const ty1 = (box.y - ray.y) / ray.dy;
const ty2 = (box.y + box.h - ray.y) / ray.dy;
const tmin = Math.max(Math.min(tx1, tx2), Math.min(ty1, ty2));
const tmax = Math.min(Math.max(tx1, tx2), Math.max(ty1, ty2));
return tmax >= 0 && tmin <= tmax;
}
// 6) Two phases
// BROADPHASE — quickly cull pairs that can't collide
// NARROWPHASE — exact test on remaining pairs
// Without broadphase: N² comparisons (1000 objects → 1M tests per frame)
// Common broadphase data structures:
// - Spatial hash / grid
// - Quadtree / Octree
// - Bounding volume hierarchy (BVH)
// - Sweep-and-prune (sort + scan)
// 7) Simple spatial hash grid
class SpatialGrid {
constructor(cellSize) {
this.cellSize = cellSize;
this.cells = new Map();
}
key(x, y) { return `${Math.floor(x / this.cellSize)},${Math.floor(y / this.cellSize)}`; }
insert(obj) {
for (let x = obj.x; x < obj.x + obj.w; x += this.cellSize) {
for (let y = obj.y; y < obj.y + obj.h; y += this.cellSize) {
const k = this.key(x, y);
if (!this.cells.has(k)) this.cells.set(k, []);
this.cells.get(k).push(obj);
}
}
}
nearby(obj) {
const result = new Set();
for (let x = obj.x; x < obj.x + obj.w; x += this.cellSize) {
for (let y = obj.y; y < obj.y + obj.h; y += this.cellSize) {
const k = this.key(x, y);
for (const o of this.cells.get(k) ?? []) result.add(o);
}
}
return [...result];
}
clear() { this.cells.clear(); }
}
// Use:
const grid = new SpatialGrid(64);
for (const o of objects) grid.insert(o);
for (const a of objects) {
for (const b of grid.nearby(a)) {
if (a !== b && aabb(a, b)) { /* collide */ }
}
}
// 8) Collision response — separate + apply impulse
// Static collision: stop the moving object at the edge
function resolveAabbStatic(moving, static_) {
const overlapX = Math.min(moving.x + moving.w - static_.x, static_.x + static_.w - moving.x);
const overlapY = Math.min(moving.y + moving.h - static_.y, static_.y + static_.h - moving.y);
if (overlapX < overlapY) {
// Push out horizontally
moving.x += (moving.x < static_.x) ? -overlapX : overlapX;
moving.vx = 0;
} else {
// Push out vertically
moving.y += (moving.y < static_.y) ? -overlapY : overlapY;
moving.vy = 0;
}
}
// 9) Elastic collision (two moving spheres)
function resolveCircles(a, b) {
const dx = b.x - a.x, dy = b.y - a.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) return;
const nx = dx / dist, ny = dy / dist;
// Separate
const overlap = a.r + b.r - dist;
a.x -= nx * overlap / 2; a.y -= ny * overlap / 2;
b.x += nx * overlap / 2; b.y += ny * overlap / 2;
// Reflect velocity along normal
const dvx = b.vx - a.vx, dvy = b.vy - a.vy;
const speed = dvx * nx + dvy * ny;
if (speed > 0) return; // already separating
a.vx += speed * nx; a.vy += speed * ny;
b.vx -= speed * nx; b.vy -= speed * ny;
}
// 10) Layers / filtering — only check what should collide
const CATEGORY = {
PLAYER: 1, ENEMY: 2, BULLET: 4, WALL: 8, PICKUP: 16,
};
function shouldCollide(a, b) {
const mask = {
[CATEGORY.PLAYER]: CATEGORY.ENEMY | CATEGORY.WALL | CATEGORY.PICKUP,
[CATEGORY.ENEMY]: CATEGORY.PLAYER | CATEGORY.WALL | CATEGORY.BULLET,
[CATEGORY.BULLET]: CATEGORY.ENEMY | CATEGORY.WALL,
};
return (mask[a.category] & b.category) !== 0;
}
// 11) Continuous collision (CCD) — for fast-moving objects
// Discrete checks miss collisions when the object moves >width in one frame.
// Solutions:
// - Smaller time steps (substepping)
// - Swept AABB — check the SHAPE of the path, not just endpoints
// - Raycast from previous to current position
function sweptAabb(a, b, vx, vy) {
// ... compute entry/exit times along x and y ...
// Returns time of collision in [0,1] or 1 if none
}
// 12) Engine-built collision (high-level)
// Unity
// Add Collider2D + Rigidbody2D
// In script: OnCollisionEnter2D / OnTriggerEnter2D
// Layer Collision Matrix: enable/disable per-layer
// Godot
// Use Area2D / Body2D + CollisionShape2D
// Signals: body_entered, area_entered
// Phaser / Pixi / Three.js
// Use built-in physics integration (Arcade, Matter.js, Cannon-es, Rapier)
// 13) Physics engines — when to use what
// Lightweight 2D : Arcade physics (Phaser)
// Realistic 2D : Matter.js, Box2D-WASM, p2.js
// 3D, realistic : Cannon-es, Rapier, Ammo.js (Bullet wrapper)
// Performance-critical 3D : Rapier (Rust, WebAssembly, very fast)
// Specialised characters : KinematicCharacterController (manual movement + collision)
// 14) Common bugs
// • Tunneling — fast object passes through thin wall (use CCD or smaller dt)
// • Jitter — object oscillates against a corner (slop tolerance in resolution)
// • Stuck in geometry — bad collision response logic; push out along proper axis
// • Inconsistent results at different framerates — use fixed timestep + interpolation
// • Triggers vs physical contact — trigger callbacks don't apply force
// 15) Common patterns
// Player walking + AABB collision with walls
function movePlayer(player, walls, dt) {
// Move X
player.x += player.vx * dt;
for (const w of walls) if (aabb(player, w)) resolveAabbStatic(player, w);
// Move Y
player.y += player.vy * dt;
for (const w of walls) if (aabb(player, w)) resolveAabbStatic(player, w);
}
// Pickup
if (aabb(player, pickup)) {
player.inventory.push(pickup.item);
pickup.collected = true;
}
// Bullet hits enemy
if (aabb(bullet, enemy)) {
enemy.hp -= bullet.damage;
bullet.dead = true;
}
// 16) Performance tips
// • Broadphase is everything — without it, N² kills performance at ~100 objects
// • Cache collision results between frames if objects haven't moved
// • Use squared distances when possible (skip sqrt)
// • For static geometry, precompute spatial structures once
// • Profile! Premature optimisation in collision = wasted time
// 17) When you should reach for a physics engine
// • Realistic motion (gravity, friction, collision response)
// • Joints (hinges, springs, ragdolls)
// • Vehicles, rope, cloth
// • Constraint solving (chained objects)
// Don't roll your own physics unless you're learning OR have a specific gameplay reason.
// 18) When to keep it simple
// • Top-down 2D game with tile collisions — AABB + grid lookup is enough
// • Arcade-style shooters — circle vs circle, AABB vs AABB
// • Platformer with simple gravity — hand-rolled AABB resolution
Why it matters
Start with AABB + a spatial grid — covers 90% of 2D games. Move to a physics engine (Matter, Rapier, Box2D) when you need realistic forces, joints, or constraints. Continuous collision (CCD) only when fast-moving objects tunnel through walls.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// AABB (axis-aligned bounding box)
function overlaps(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;
}
Try it Yourself »
Exercise
AABB stands for "____-Aligned Bounding Box".
Four letters.
Discussion
Loading…