Collision Response: When Objects Collide
What You'll Learn
In the previous tutorial, we made balls bounce off walls. But what happens when two balls hit each other? That's where collision response becomes crucial.
In this tutorial, you'll learn:
- How to detect when two circles overlap
- How to separate overlapping objects
- How to calculate realistic bounce angles
- How to transfer momentum between objects
Circle vs Circle Detection
Detecting if two circles collide is surprisingly simple: just check if the distance between their centers is less than the sum of their radii.
function circlesCollide(a, b) {
const dx = b.x - a.x;
const dy = b.y - a.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const minDistance = a.radius + b.radius;
return distance < minDistance;
}
// Usage
if (circlesCollide(ball1, ball2)) {
// Handle collision!
}
You can skip the expensive Math.sqrt() by comparing squared distances:
dx*dx + dy*dy < minDist*minDist
Separating Overlapping Objects
When objects overlap, we need to push them apart. The key is to move them along the collision normal (the line connecting their centers).
function separateCircles(a, b) {
const dx = b.x - a.x;
const dy = b.y - a.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const minDistance = a.radius + b.radius;
if (distance < minDistance) {
// Calculate overlap
const overlap = minDistance - distance;
// Normalize the collision vector
const nx = dx / distance;
const ny = dy / distance;
// Push each circle by half the overlap
a.x -= nx * overlap / 2;
a.y -= ny * overlap / 2;
b.x += nx * overlap / 2;
b.y += ny * overlap / 2;
}
}
Always separate objects before applying velocity changes. Otherwise, they might get stuck inside each other!
Elastic Collision Response
In an elastic collision, kinetic energy is conserved. The objects bounce off each other like billiard balls.
Don't worry if the formula looks complex! Here's the practical implementation:
function resolveCollision(a, b) {
const dx = b.x - a.x;
const dy = b.y - a.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// Collision normal (unit vector)
const nx = dx / distance;
const ny = dy / distance;
// Relative velocity
const dvx = a.vx - b.vx;
const dvy = a.vy - b.vy;
// Relative velocity along collision normal
const dvn = dvx * nx + dvy * ny;
// Don't resolve if velocities are separating
if (dvn > 0) return;
// Restitution (bounciness): 1 = perfectly elastic
const restitution = 0.9;
// Calculate impulse scalar (assuming equal mass)
const impulse = -(1 + restitution) * dvn / 2;
// Apply impulse to both balls
a.vx += impulse * nx;
a.vy += impulse * ny;
b.vx -= impulse * nx;
b.vy -= impulse * ny;
}
Understanding the Code
- Collision Normal: The direction from circle A to circle B
- Relative Velocity: How fast A is approaching B
- Dot Product: How much of that velocity is along the collision normal
- Impulse: The "push" we need to apply to both objects
Interactive Demo
Click anywhere to add a new ball. Watch them collide and bounce off each other!
Adding Mass
In real physics, heavier objects transfer more momentum. Here's how to handle different masses:
function resolveCollisionWithMass(a, b) {
const dx = b.x - a.x;
const dy = b.y - a.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const nx = dx / distance;
const ny = dy / distance;
const dvx = a.vx - b.vx;
const dvy = a.vy - b.vy;
const dvn = dvx * nx + dvy * ny;
if (dvn > 0) return;
const restitution = 0.9;
// Mass-weighted impulse
const impulse = -(1 + restitution) * dvn / (1/a.mass + 1/b.mass);
// Apply impulse proportional to inverse mass
a.vx += (impulse / a.mass) * nx;
a.vy += (impulse / a.mass) * ny;
b.vx -= (impulse / b.mass) * nx;
b.vy -= (impulse / b.mass) * ny;
}
// Example: bigger ball is heavier
const ball = {
x: 100, y: 100,
vx: 2, vy: 0,
radius: 30,
mass: 30 // Mass proportional to radius
};
A heavy object hitting a light one will barely slow down, while the light object flies away. A light object hitting a heavy one bounces back with almost the same speed!
Complete Implementation
Here's the full multi-ball collision system you can use in your games:
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const GRAVITY = 0.2;
const BOUNCE = 0.9;
const FRICTION = 0.999;
const balls = [];
function createBall(x, y) {
const radius = 15 + Math.random() * 20;
return {
x, y,
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 8,
radius,
mass: radius,
color: `hsl(${Math.random() * 360}, 80%, 60%)`
};
}
function update() {
for (const ball of balls) {
// Apply gravity
ball.vy += GRAVITY;
// Apply friction
ball.vx *= FRICTION;
ball.vy *= FRICTION;
// Update position
ball.x += ball.vx;
ball.y += ball.vy;
// Wall collisions
if (ball.x - ball.radius < 0) {
ball.x = ball.radius;
ball.vx *= -BOUNCE;
}
if (ball.x + ball.radius > canvas.width) {
ball.x = canvas.width - ball.radius;
ball.vx *= -BOUNCE;
}
if (ball.y + ball.radius > canvas.height) {
ball.y = canvas.height - ball.radius;
ball.vy *= -BOUNCE;
}
}
// Check all pairs for collisions
for (let i = 0; i < balls.length; i++) {
for (let j = i + 1; j < balls.length; j++) {
handleCollision(balls[i], balls[j]);
}
}
}
function handleCollision(a, b) {
const dx = b.x - a.x;
const dy = b.y - a.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const minDist = a.radius + b.radius;
if (distance < minDist) {
// Separate
const overlap = minDist - distance;
const nx = dx / distance;
const ny = dy / distance;
const totalMass = a.mass + b.mass;
a.x -= nx * overlap * (b.mass / totalMass);
a.y -= ny * overlap * (b.mass / totalMass);
b.x += nx * overlap * (a.mass / totalMass);
b.y += ny * overlap * (a.mass / totalMass);
// Resolve collision
const dvx = a.vx - b.vx;
const dvy = a.vy - b.vy;
const dvn = dvx * nx + dvy * ny;
if (dvn > 0) return;
const impulse = -(1 + BOUNCE) * dvn / (1/a.mass + 1/b.mass);
a.vx += (impulse / a.mass) * nx;
a.vy += (impulse / a.mass) * ny;
b.vx -= (impulse / b.mass) * nx;
b.vy -= (impulse / b.mass) * ny;
}
}
function render() {
ctx.fillStyle = '#0A0A12';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (const ball of balls) {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = ball.color;
ctx.fill();
}
}
function gameLoop() {
update();
render();
requestAnimationFrame(gameLoop);
}
// Initialize with some balls
for (let i = 0; i < 5; i++) {
balls.push(createBall(
100 + Math.random() * 300,
100 + Math.random() * 200
));
}
gameLoop();
Challenges
- Pool Game: Create a simple pool game with a cue ball you can shoot
- Color Transfer: When balls collide, blend their colors together
- Size Change: When balls collide, the bigger one absorbs the smaller one
- Chain Reaction: Make balls explode into smaller balls on hard impacts
In the next tutorial, we'll use these collision techniques to build Platformer Physics – jump curves, coyote time, and wall sliding!