Lesson 14
Racing physics: make cars feel like vehicles, not sliding icons.
A browser racing game does not need a full simulation to feel good. It needs acceleration, steering that depends on speed, friction, track feedback and collisions that slow the car without making it unpredictable.
1. Use heading plus speed for arcade handling
The simplest playable racing model stores `x`, `y`, `angle`, `speed`, `accel` and `steer`. Acceleration changes speed. Steering changes angle. Position changes by moving forward along the current angle. This is not physically complete, but it is understandable and easy to tune.
function updateCar(car, dt) {
const frame = dt / 16.67;
const maxSpeed = car.boostTimer > 0 ? 7 : 4.5;
const friction = car.onTrack ? 0.97 : 0.92;
car.angle += car.steer * (car.speed / maxSpeed) * 0.035 * frame;
car.speed += car.accel * 0.25 * frame;
car.speed *= Math.pow(friction, frame);
car.speed = Math.max(-1.5, Math.min(maxSpeed, car.speed));
car.x += Math.cos(car.angle) * car.speed * frame;
car.y += Math.sin(car.angle) * car.speed * frame;
}
Source pattern: Supagames level 05 Racing uses heading, speed, friction, boosts, oil slicks and track boundary correction for a fast two-player race.
The key detail is `car.speed / maxSpeed` in steering. A stopped car should not spin instantly like a compass needle. A moving car should turn more strongly because the tires have motion to redirect.
2. Separate input from physics
Input should set intent, not directly move the car. Keyboard, touch buttons or gamepad controls can all update `accel` and `steer`; the physics function then applies those values. This makes mobile controls easier because touch buttons do not need a separate movement system.
function readRacingInput(car, keys) {
car.accel = 0;
car.steer = 0;
if (keys.up || keys.w) car.accel = 1;
if (keys.down || keys.s) car.accel = -1;
if (keys.left || keys.a) car.steer = -1;
if (keys.right || keys.d) car.steer = 1;
}
function bindTouchButton(button, down, up) {
button.addEventListener("pointerdown", function (event) {
event.preventDefault();
down();
});
button.addEventListener("pointerup", function () {
up();
});
}
For phones, buttons should sit outside the most important track area or fade when inactive. A racing game can be ruined by controls covering corners, checkpoints or rival cars.
3. Make off-road behavior readable
Players understand "grass slows me down" immediately if the car loses speed and maybe emits dust. They do not understand invisible walls that randomly bounce them backward. For an arcade track, detect whether the car is on the track, lower friction off-road and use particles or color changes as feedback.
function applyTrackSurface(car, track) {
car.onTrack = isOnTrack(car.x, car.y, track);
if (!car.onTrack) {
car.speed *= 0.985;
spawnDust(car.x, car.y);
}
}
function spawnDust(x, y) {
particles.push({
x, y,
vx: (Math.random() - 0.5) * 1.5,
vy: (Math.random() - 0.5) * 1.5,
life: 22,
color: "#c8a46a"
});
}
If the track has hard boundaries, push the car back gently and reduce speed. Avoid large teleports. The player should see that they hit the edge and lost momentum, not feel like the game corrected them in secret.
4. Add boosts and hazards as timed modifiers
Racing powerups are easiest to maintain when they are timers. A speed boost sets `boostTimer`; oil sets `oilTimer`; a repair zone sets `repairTimer`. The physics update checks those timers and adjusts max speed, steering or friction. This avoids scattering special cases across rendering and collision code.
function collectBoost(car) {
car.boostTimer = 1800;
car.speed = Math.max(car.speed, 3.5);
}
function hitOil(car) {
car.oilTimer = 900;
}
function applyModifiers(car, dt) {
if (car.boostTimer > 0) car.boostTimer -= dt;
if (car.oilTimer > 0) {
car.oilTimer -= dt;
car.steer *= 0.35;
car.speed *= 0.995;
}
}
Keep hazards readable. If oil reduces steering, show a skid particle or wobble. If boost increases max speed, add a glow or exhaust. Feedback turns a number change into a game event.
5. Handle car-to-car collisions with momentum exchange
In multiplayer or AI racing, cars should not pass through each other. A simple circle collision works well for top-down racers. Separate the cars along the collision normal, then exchange a portion of their speeds. You do not need perfect physics; you need believable contact.
function resolveCarCollision(a, b) {
const dx = b.x - a.x;
const dy = b.y - a.y;
const dist = Math.hypot(dx, dy) || 1;
const minDist = 28;
if (dist >= minDist) return;
const nx = dx / dist;
const ny = dy / dist;
const push = (minDist - dist) / 2;
a.x -= nx * push;
a.y -= ny * push;
b.x += nx * push;
b.y += ny * push;
const relative = (a.speed - b.speed) * 0.5;
a.speed -= relative * 0.3;
b.speed += relative * 0.3;
}
After collisions, briefly reduce steering or add screen shake only if it helps clarity. Too much shake in racing makes the track harder to read, especially on mobile.
6. When to use deeper physics
Supagames also has more advanced car physics experiments with velocity vectors, angular velocity, gear ratios, tire friction and terrain normal checks. Those are useful when the game is about handling depth. For most casual browser racing games, start with the arcade model above. It is easier to tune and easier for players to understand.
If you add gears, suspension or drift, expose the reason in the UI. A speedometer, lap counter, checkpoint markers and clear off-road feedback do more for playability than hidden realism. The final goal is not a scientific car; it is a vehicle the player wants to master.