Fixed Timestep Game Loop: Physics Jitter, Interpolation, and Catch-Up Limits
Separate simulation time from render time with a fixed-timestep accumulator that stays stable through variable frame rates and hitches. This guide builds a practical C++20 loop with interpolation, catch-up limits, input handling, and engine-specific guidance.
# Fixed Timestep Game Loop: Physics Jitter, Interpolation, and Catch-Up Limits
A fixed timestep game loop solves a problem that multiplying every movement value by deltaTime does not: some systems should not receive an arbitrarily large simulation step just because rendering became slow for one frame.
The core idea is to separate two clocks. Rendering can run whenever the machine is ready to present a frame, while gameplay simulation advances in fixed increments such as 1/60 second. An accumulator connects those two clocks.
That separation gives physics, collision detection, gameplay timers, and deterministic systems a more predictable time domain. It also creates new questions: what happens after a long hitch, how many updates should run in one frame, how do you remove visible physics jitter, and when should input be consumed?
This guide builds a practical fixed-timestep architecture and explains the failure cases that matter in a real game.
## Why Variable Delta Time Is Not Enough
A typical variable-step loop looks like this:
```cpp
while (running)
{
const double dt = measure_frame_time();
update(dt);
render();
}
```
For simple movement, multiplying by elapsed time is correct:
```cpp
position += velocity * dt;
```
That prevents an object from moving faster simply because the renderer produces more frames.
The problem appears when dt becomes unusually large. A normal sequence might look like:
```text
0.016
0.017
0.015
0.016
```
Then a hitch suddenly produces:
```text
0.180
```
If that value is passed directly into every simulation system, the game is asking physics, collision detection, springs, movement code, and gameplay timers to advance 180 milliseconds in one jump.
Possible results include collision tunneling, unstable constraints, inconsistent acceleration, missed triggers, large camera movements, or visibly different behavior depending on frame rate.
Variable delta time is still useful. UI animation, camera presentation, cosmetic effects, and some non-physical movement can work perfectly well with it.
The mistake is treating render-frame duration as the only clock in the engine.
## Fixed Simulation, Variable Rendering
Choose a fixed simulation interval:
```cpp
constexpr double fixedDt = 1.0 / 60.0;
```
Every simulation tick receives exactly the same dt.
The renderer remains independent. A frame rendered around 30 FPS may require roughly two simulation ticks. At 144 FPS, several rendered frames may occur before another simulation tick becomes due.
An accumulator stores elapsed real time until enough exists to execute a fixed update.
Conceptually:
```text
Uneven render time:
|------|---|---------|----|
Accumulator:
[ elapsed time waiting to be simulated ]
Fixed simulation:
|----|----|----|----|----|
```
Real elapsed time enters the accumulator. Simulation consumes it in equal chunks.
## A Practical C++20 Fixed Timestep Loop
```cpp
#include
#include
#include
using Clock = std::chrono::steady_clock;
constexpr double kFixedDt = 1.0 / 60.0;
constexpr double kMaxFrameTime = 0.25;
constexpr int kMaxCatchUpSteps = 8;
State current = make_initial_state();
State previous = current;
double accumulator = 0.0;
auto previousWallTime = Clock::now();
while (running())
{
poll_platform_events();
input.capture_frame();
const auto now = Clock::now();
double frameTime =
std::chrono::duration(
now - previousWallTime
).count();
previousWallTime = now;
frameTime =
std::clamp(frameTime, 0.0, kMaxFrameTime);
accumulator += frameTime;
int steps = 0;
while (accumulator >= kFixedDt &&
steps < kMaxCatchUpSteps)
{
previous = current;
const TickInput tickInput =
input.for_tick();
simulate(current, tickInput, kFixedDt);
accumulator -= kFixedDt;
++steps;
}
if (accumulator >= kFixedDt)
{
accumulator =
std::fmod(accumulator, kFixedDt);
report_simulation_overrun();
}
const double alpha =
accumulator / kFixedDt;
const RenderState visible =
interpolate(previous, current, alpha);
render(visible);
}
```
The syntax is not the important part. The important part is that the loop defines explicit policies for elapsed time, catch-up work, and presentation.
## What the Accumulator Represents
At 60 Hz, one simulation tick lasts about 16.67 milliseconds.
Suppose a rendered frame takes 10 ms. The accumulator now contains 10 ms, which is not enough for a simulation step.
The next frame takes another 10 ms. The accumulator reaches 20 ms. One 16.67 ms simulation tick executes, leaving approximately 3.33 ms.
That remainder should normally be preserved.
If the engine cleared the accumulator after every rendered frame, fractions of elapsed time would repeatedly disappear. Simulation speed could then drift depending on rendering behavior.
The accumulator is therefore more than a timer. It is the bookkeeping that keeps fixed simulation synchronized with real elapsed time.
## Why You Need a Frame-Time Clamp
This line:
```cpp
frameTime =
std::clamp(frameTime, 0.0, kMaxFrameTime);
```
protects the simulation from extreme wall-clock jumps.
A debugger pause, application suspend, asset-loading stall, blocked operating-system event, or severe hitch can produce seconds of elapsed time.
Without a clamp, returning from a five-second pause could make the game attempt hundreds of physics updates.
For most offline games, that is undesirable.
A frame-time clamp effectively says that beyond a chosen threshold, the elapsed time is treated as a discontinuity rather than normal simulation debt.
The 0.25 second value above is only an example. It is not a universal constant. Different games may choose very different behavior.
## Why You Also Need a Catch-Up Limit
The frame-time clamp limits how much time enters the accumulator. A catch-up limit controls how much simulation work can happen in one rendered frame.
That is the purpose of:
```cpp
steps < kMaxCatchUpSteps
```
Without it, the engine could execute a large number of simulation steps after a hitch.
That creates a failure pattern often called the spiral of death.
A frame becomes slow, so additional simulation debt accumulates. The next frame tries to process several simulation ticks. Those ticks make the next frame slow as well. Even more simulation debt accumulates, and the game struggles to recover.
A fixed timestep only works sustainably when the machine can normally process simulation time faster than real time passes.
If one 16.67 ms simulation tick consistently requires 20 ms of CPU time, no accumulator design can permanently solve the problem. The simulation workload itself is too expensive.
## Decide What Happens When the Game Falls Behind
Once the catch-up limit is reached, the engine needs an explicit policy.
The example discards whole overdue simulation ticks while keeping the fractional remainder:
```cpp
accumulator =
std::fmod(accumulator, kFixedDt);
```
This bounds CPU work and allows rendering to recover.
The trade-off is that simulation temporarily advances more slowly than wall-clock time.
For many offline games, a small amount of time dilation during a severe hitch is preferable to freezing for several additional frames.
Another option is keeping all accumulated debt and attempting to catch up later. That preserves elapsed simulation time but risks repeated slow frames if the game does not have enough spare CPU capacity.
Networked or authoritative simulations may require a different strategy entirely. A client might need to resynchronize with the server, apply an authoritative state correction, or reduce nonessential work while catching up.
The important point is not that one strategy is universally correct. The important point is that the failure policy is deliberate.
## Why Fixed Physics Can Still Look Jittery
A stable fixed simulation can still look jerky.
Imagine physics updating at 60 Hz while the display renders at 144 Hz.
The authoritative position changes only when a simulation tick completes. If the renderer simply draws the latest simulation transform, several render frames may show exactly the same position before the object jumps to the next one.
The simulation is correct.
The presentation is not smooth.
Interpolation solves that problem.
## Render Between Two Simulation States
Store the state before the newest simulation update:
```cpp
previous = current;
simulate(current, tickInput, kFixedDt);
```
After the fixed-step loop finishes, calculate:
```cpp
const double alpha =
accumulator / kFixedDt;
```
Because the accumulator contains less than one complete fixed step under normal conditions, alpha remains between zero and one.
For a scalar:
```cpp
double lerp(double a, double b, double alpha)
{
return a + (b - a) * alpha;
}
```
For a position:
```cpp
Vec3 visiblePosition =
previous.position +
(current.position - previous.position) *
alpha;
```
The renderer draws the interpolated result.
The simulation itself still uses `current`.
Do not write the interpolated position back into authoritative physics state. Otherwise presentation begins affecting the next simulation step.
## Interpolation Has a Latency Trade-Off
Interpolation works by displaying a state between two completed simulation snapshots.
That means presentation is slightly behind the newest simulation state.
At a 60 Hz simulation rate, that delay is roughly bounded by one simulation interval.
For many objects, the trade-off is worthwhile because motion appears significantly smoother.
Extrapolation takes a different approach. Instead of rendering between known states, it predicts beyond the newest state.
That can reduce apparent delay, but predictions become incorrect when an object suddenly collides, changes direction, teleports, stops, or receives a network correction.
Interpolation is conservative.
Extrapolation is predictive.
They solve related problems with different risks.
## Do Not Interpolate Every Property
Position, scale, camera targets, and similar continuous values are reasonable interpolation candidates.
Discrete gameplay state should usually not be blended.
Examples include:
```text
isDead
weaponId
doorLocked
animationState
activeAbility
collisionLayer
```
A character cannot be 40% dead because interpolation alpha happens to be 0.4.
Rotations also require appropriate rotational interpolation rather than blindly interpolating arbitrary Euler angles.
The presentation layer should know which properties represent continuous state and which represent discrete transitions.
## Reset Interpolation After Teleports
Interpolation assumes the previous and current simulation states describe continuous movement.
A teleport breaks that assumption.
Suppose an object moves instantly from (0, 0) to (100, 0).
If interpolation history is preserved, the renderer may draw the object moving through every intermediate position even though the simulation teleported it instantly.
After a teleport, respawn, portal transition, or major network correction, reset the previous state:
```cpp
current.position = teleportTarget;
previous.position = current.position;
```
Now there is no old path for the renderer to blend across.
This small detail prevents many one-frame streaks and camera jumps.
## Input Runs on Another Clock
Separating rendering and simulation also creates an input problem.
Continuous input is straightforward:
```text
Move left held
Trigger held
Analog stick position
```
The input system can store the newest state and expose it to each simulation tick.
One-shot input needs more care:
```text
Jump pressed
Reload pressed
Mouse button released
Scroll wheel moved
```
If a button is pressed and released between two fixed simulation updates, reading input only inside the fixed loop can miss the event completely.
A robust input system should latch, queue, or timestamp edge events until the simulation consumes them.
Mouse movement and similar deltas also require a clear policy. If three catch-up simulation ticks run in one rendered frame, the same mouse delta should not accidentally be applied three times.
Possible approaches include accumulating relative input until the next simulation tick, timestamping input events, or associating input packets with simulation tick numbers.
## Fixed Timestep Does Not Guarantee Determinism
A fixed dt removes one major source of simulation variation.
It does not automatically make the game deterministic.
Results can still diverge because of:
- floating-point differences
- random-number generation
- unordered container iteration
- physics solver ordering
- compiler differences
- platform-specific mathematics
- race conditions
- nondeterministic job scheduling
A replay system also requires the same initial state and the same ordered input sequence.
Deterministic lockstep multiplayer requires even stricter control.
Treat a fixed timestep as one useful requirement for reproducibility, not proof that the simulation is deterministic.
## What Should Run in the Fixed Tick?
Systems that commonly benefit from fixed simulation timing include:
- physics integration
- collision response
- character simulation
- deterministic gameplay rules
- simulation timers
- rollback progression
- replay state progression
Systems commonly better suited to frame-oriented timing include:
- UI animation
- cosmetic particles
- final camera smoothing
- purely visual effects
- editor overlays
- frame presentation
Not every subsystem needs one universal update rate.
Networking, AI, animation, audio, and background systems may each use their own update schedules depending on the engine.
## Choosing the Fixed Tick Rate
There is no universal best simulation frequency.
A higher tick rate creates smaller simulation steps.
Potential advantages include finer collision resolution, lower simulation latency, and better behavior for fast-moving objects.
The cost is additional CPU work because more simulation updates must execute every second.
A lower frequency reduces CPU cost but makes each step represent more elapsed time and movement.
A sensible process is:
1. Start from gameplay requirements.
2. Measure the cost of one simulation tick.
3. Test fast-moving interactions.
4. Test slow and fast rendering.
5. Inject deliberate hitches.
6. Measure input latency.
7. Tune the rate using evidence.
Do not choose 120 Hz simply because the display runs at 120 Hz.
Display refresh rate and simulation frequency solve different problems.
## Unity, Godot, and Unreal Engine
Major engines already expose versions of these concepts.
In Unity, physics-oriented gameplay is commonly handled through `FixedUpdate`, while `Time.fixedDeltaTime` controls the fixed interval. Rigidbody interpolation can smooth visible movement between physics updates.
In Godot, physics-dependent logic belongs in `_physics_process()`. Godot's physics interpolation system can smooth presentation between physics ticks.
In Unreal Engine, physics sub-stepping can divide physics work into smaller steps when larger frame intervals would reduce simulation accuracy or stability.
The exact implementation differs between engines, so a custom accumulator should not be wrapped around built-in physics without understanding the engine's timing model.
The broader architecture remains the same: simulation timing and presentation timing are related, but they are not the same thing.
## Measure More Than FPS
Average FPS can appear healthy while simulation timing is unhealthy.
Useful telemetry includes:
```text
simulation steps this frame
simulation tick duration
accumulator depth
catch-up limit hits
frame-time clamp events
interpolation alpha
input events consumed
```
For example, a game could alternate between zero and three simulation steps per rendered frame while reporting an acceptable average frame rate.
That timing pattern can still produce poor responsiveness or visible instability.
A graph showing accumulator depth and simulation steps per frame often reveals problems that an FPS counter hides.
## Test Hostile Timing Conditions
Do not validate the loop only on a powerful development machine near the intended frame rate.
Test it under deliberately mismatched conditions:
- rendering below the simulation rate
- rendering approximately equal to the simulation rate
- rendering far above the simulation rate
- uncapped rendering
- deliberate 50 ms hitches
- deliberate 100 ms hitches
- deliberate 250 ms hitches
- debugger pauses
- application suspend and resume
- heavy asset streaming
- rapid teleports or respawns
- high-frequency input
Watch for growing accumulator debt, repeated catch-up-limit events, repeated one-shot inputs, physics instability, camera jitter, and incorrect interpolation after teleports.
Timing bugs that remain hidden at 60 FPS often become obvious at 37 FPS or 144 FPS.
## Final Checklist
Before calling a custom fixed timestep loop production-ready, verify that:
- elapsed time comes from a monotonic clock
- simulation always receives the intended fixed dt
- rendering and simulation use separate timing domains
- fractional accumulator time is preserved
- pathological wall-time gaps are handled
- catch-up work has a defined limit or recovery policy
- interpolation affects presentation rather than authoritative simulation
- teleports reset interpolation history
- one-shot input survives until a simulation tick consumes it
- catch-up ticks do not repeat one input event accidentally
- simulation cost is measured on representative hardware
- fixed timestep is not treated as automatic determinism
- the loop is tested at intentionally mismatched render and simulation rates
## Conclusion
The useful mental model for a fixed timestep game loop is not simply "run physics at 60 FPS."
A robust game loop has separate simulation and presentation clocks.
The accumulator converts irregular elapsed time into fixed simulation steps. A hitch policy prevents extreme wall-clock gaps from becoming enormous update queues. A catch-up limit protects frame time. Interpolation converts discrete simulation states into smooth visual motion. Input handling bridges events between the two timing domains.
Once those pieces are designed together, frame-rate changes become an expected operating condition instead of a source of mysterious physics behavior.