Integration and timestep
Integration answers "given this thing's velocity and acceleration, where is it next frame?" It is three lines of code, and which three decides whether your springs settle or detonate. How big a slice of time each step covers — and whether that size changes — is the other half of the question, and the half that causes bugs you can't reproduce.
Pick the right three lines
Explicit (forward) Euler reads velocity before updating it, and gains energy for it.
pos += vel * dt; // uses the OLD velocity
vel += acc * dt;
Because the position step uses a velocity that is already out of date, the error always leans the same way: it accumulates instead of averaging out. An orbit spirals outward; a spring's amplitude grows every cycle until it explodes.
Semi-implicit (symplectic) Euler swaps the two lines, and that's the whole fix.
vel += acc * dt; // velocity first
pos += vel * dt; // then move with the NEW velocity
Same cost, same line count. Energy now wobbles around the true value instead of climbing away from it. This is what virtually every game physics engine actually runs, and it is the default you should reach for.
For context on the other two names: Verlet (and its velocity-Verlet variant) stores the previous position instead of a velocity — popular for cloth and particles because constraints can be applied by just moving points. RK4 samples the derivative four times per step for much higher accuracy, which is wonderful for a smooth orbit and nearly useless for games, where collisions make the force discontinuous mid-step anyway.
Fix the timestep
Never feed real frame time straight into physics. A variable dt means the simulation takes different paths on different machines, so nothing is deterministic and replays desync. Worse, stiff things — springs, stacks, constraints — are only stable below a dt ceiling, so one long frame is enough to blow them up.
The standard loop decouples simulation rate from frame rate:
acc += frameTime;
let steps = 0;
while (acc >= FIXED_DT && steps++ < MAX_STEPS) {
simulate(FIXED_DT); // always the same dt
acc -= FIXED_DT;
}
render(acc / FIXED_DT); // interpolate between last two states
FIXED_DT is typically 1/60 or 1/120. The leftover acc is the fraction of a step you haven't simulated yet, so render each body lerped between its previous and current state — otherwise a 144 Hz display showing a 60 Hz simulation stutters.
MAX_STEPS is the death-spiral guard. If catching up takes longer than the time it caught up, the next frame owes even more steps, and the game locks solid. Clamping the catch-up drops simulation time instead: a moment of slow motion, then recovery.
Tunnelling
A bullet moving 400 m/s travels 6.7 m in one 60 Hz step, so a 10 cm wall simply isn't there on either side of it. Discrete detection tests positions, not the path between them. The fixes, cheapest first: cap velocities; take substeps (several small steps per frame for fast bodies); or use swept / continuous tests (sweep the shape along its motion and solve for the earliest time of impact). Continuous is the correct answer and the expensive one — most engines enable it only for bodies flagged as bullets.
Demo
Three integrators, a jitter toggle, and a bullet versus a wall — watch the energy graph: explicit Euler's line only ever climbs. Then turn on frame-time jitter with a variable timestep, and push the bullet's speed up with substeps at 1.
Gotchas
| Symptom | Cause |
|---|---|
| Spring or ragdoll explodes on a hitch | Variable dt exceeded the stability limit; fix the timestep |
| Physics behaves differently on a faster PC | Frame-rate-dependent dt, or accumulated float order differing |
| Game freezes solid under load | No MAX_STEPS clamp — death spiral |
| Motion looks stuttery despite high FPS | Fixed steps rendered without interpolation |
| Orbits slowly spiral out; stacks creep upward | Explicit Euler. Swap the two lines |
| Fast projectiles miss thin geometry | Tunnelling; substep, sweep, or cap speed |
| Everything slows down instead of dropping frames | Catch-up clamp is working as designed — that's the trade |