Deterministic fixed-step physics in Unity without fighting the engine
Rollback netcode does not need a custom physics engine. It needs you to stop reading Time.deltaTime.
Contents
Every determinism bug we shipped last year had the same shape: something in gameplay read Time.deltaTime instead of Time.fixedDeltaTime, and two clients running at 60 and 120 fps disagreed about where the player was four seconds later. Not by much. By enough.
The rule that fixed 90% of it
Simulation code lives in FixedUpdate and may only read fixed-step time. Presentation code lives in Update and may never write simulation state. We enforce it with an assembly definition: the Gameplay.Sim assembly does not reference UnityEngine.Input or the renderer at all, so the compiler catches the mistake instead of QA catching it three builds later.
// Sim assembly. No Update(), no deltaTime, no Random.value.
void FixedUpdate() {
var input = _inputBuffer.ForTick(_tick);
_state = Step(_state, input, FixedDt); // FixedDt is a const, not a property
_tick++;
}Floats are fine, actually
The usual advice is to move to fixed-point maths. We did not, and we did not need to. IEEE 754 is deterministic for the same operations in the same order on the same instruction set — the danger is not the float, it is the ordering. Our real culprits were, in order of how often they bit us:
- Iterating a
Dictionaryin the simulation. Insertion order is not guaranteed across runs. Replaced with a sorted entity id array. Physics.autoSimulationleft on, so PhysX stepped on the render frame instead of our tick.Random.valueinstead of a seeded xorshift carried in the simulation state.- Coroutines. A coroutine resumes on a frame boundary, which is exactly the thing we are trying not to depend on.
How we test it
A headless test runs the same 1,800 ticks of recorded input twice, at different frame pacing, and hashes the simulation state every 60 ticks. If the hash streams diverge, the test prints the first differing tick and the entity whose position moved. That test has caught four regressions this year, all of them within a day of the commit that caused them.
The value of determinism is not netcode. It is that a bug report becomes a seed and a tick number.
Set Physics.simulationMode to Script, step it yourself, and hash your state. That is most of it.
Written by
Priya Raman
Gameplay engineer, seven years in Unity. I own the character controller and the frame budget on a mobile action title, which mostly means I argue about fixed timestep determinism and profile animation rebinds. Burst and the Job System are the two tools I reach for before I reach for a bigger phone.
6 Comments
Sign in to join the discussion
One caveat on 'IEEE 754 is deterministic': that holds for the same operations in the same order on the same instruction set, and stops holding the moment FMA contraction is enabled on one target and not the other. Our training-versus-serving skew bug was exactly this, x86 with FMA against ARM without.
The
Dictionaryiteration one is the same bug as relying on row order without anORDER BY. It works for two years and then the storage layer reorganises and it 'randomly' breaks in production. Sorted entity ids is the right fix.