iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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".

Test yourself

Q1. AABB stands for…
Q2. For complex shapes prefer…
Q3. Continuous Collision Detection helps…

Discussion

Loading…