Lindsay Edwards

Tune the timestep to the fastest thing

I built a retro-styled racer as a side project, partly to get away from backends for a bit. Early on the handling felt wrong in two specific ways. At speed the car would occasionally clip straight through a wall, and after a jump it would misread the landing, sometimes reading as airborne while sitting flat on the ground.

Coming from backend work, I assumed my collision logic was buggy. I went looking for the bug. There wasn’t one. The logic was fine. It just was not running often enough.

The default tick was too slow for the car#

The engine’s default physics tick is 60 steps per second. That is plenty for most things. It was not plenty for a car moving fast enough to cross a wall’s thickness in less than one step.

That is tunnelling. Between one physics frame and the next, the car travels far enough that at no single sampled instant is it actually inside the wall. Frame N it is in front, frame N plus one it is behind, and the collision that should have happened in the gap simply never gets checked. It is a speed camera that photographs the road once a second: a car moving fast enough slips through between shots and is never caught. Nothing is wrong with the check. The check is just not looking often enough to catch a fast object.

So I doubled the tick to 120 steps per second and kept all the handling in the fixed physics callback, decoupled from render frame rate.

# fixed step, independent of how fast we happen to render
Engine.physics_ticks_per_second = 120

The tunnelling mostly stopped. Sampling twice as often halves the distance the car covers between checks, so the wall gets seen. Same code, more frequent looks.

Coyote time for the landings#

The landing problem was different but related. Skimming a bump at speed would lift the car off the ground for a single tick, and that one airborne frame was enough to flip the “on ground” state and mangle the handling.

I added a short grace period. Platformers call it coyote time. When the car leaves the ground, it stays “grounded” for a few milliseconds before it actually counts as airborne. Brush a bump and the grace window absorbs it. Actually launch off a ramp and the window expires and you are properly in the air.

Between the higher tick and the grace period, launches and landings got crisp. Neither was clever. Both were about matching the simulation’s timing to how fast things actually move.

Fixed-timestep simulation should be tuned to the fastest thing in your world, not left at whatever default shipped in the box.

The part I want to be honest about is the cost. Doubling the tick does not come free. I am now running the entire physics step twice as often, which is twice the CPU spent on simulation every second. On the cheap phones I was targeting, that is real money out of a small budget.

That is the actual trade. I bought determinism and fewer missed collisions, and I paid for them in CPU time. Worth it here, because a racer that clips through walls is broken in a way players notice immediately. Not always worth it. The point is that it is a choice with a price, not a default to leave alone and forget.

This generalises past games. Any accumulator-driven loop has the same knob: physics, a control system, a sensor poll, a rate limiter that ticks on a clock. The default rate is someone else’s guess about someone else’s fastest event. If your fastest event is faster than that, you will miss things in the gaps, and no amount of smarter logic inside the step fixes a step that runs too rarely. Tune the rate to the fastest thing you care about, then pay the bill knowingly.

Keep reading