If you come from building web services or desktop line-of-business apps, the game loop is one of the stranger things you will meet in game development. It looks simple (run forever, update the world, draw the world) but a surprising amount of details hide in how you measure the time between those updates. This post walks through the problem and the small class my Mythic Maze game uses to solve it, the FixedStepClock.
The simplest game loop, and why it breaks
The first version of a game loop almost everyone writes looks like this:
var last = Stopwatch.GetTimestamp();
while (running)
{
var now = Stopwatch.GetTimestamp();
var deltaSeconds = (now - last) / (double)Stopwatch.Frequency;
last = now;
world.Update(deltaSeconds);
Render(world);
}
Every frame, you measure how long the previous frame took and hand that number to the simulation. The simulation scales its movement by it. A player moving at 4 tiles per second moves 4 * deltaSeconds tiles. This is called a variable timestep, and it is intuitive: faster machines get smoother motion, slower machines still see the game run at the right speed.
It is also the source of several bugs that are genuinely hard to track down.
Floating point error accumulates. deltaSeconds is a double that is almost never a clean value – it is 0.016666…, or 0.0171233…, or whatever the display and the OS scheduler happened to produce. Add thousands of those together over a few minutes of play and the total drifts away from the true elapsed time. For a shooter, nobody notices. For a puzzle game with a countdown timer and a leaderboard, two players who played identically can finish with different times.
The simulation is not reproducible. This is the big one. If the result of Update depends on deltaSeconds, and deltaSeconds depends on how busy the machine was, then running the same inputs twice gives two different outcomes. That kills replays, it kills deterministic multiplayer, and it makes a whole category of bug reports impossible to reproduce. You cannot write a test that says “given this level and this sequence of key presses, the player ends on tile (7, 3)” because the answer depends on the frame rate of whatever machine ran the test.
Large deltas break collision detection. Suppose a frame takes 400 ms because the OS decided to page something in, or the player dragged the window, or a garbage collection ran long. Your fast-moving actor now advances 400 ms worth of distance in a single step. If a wall was 200 ms of travel away, the actor is now on the far side of it. The collision check never saw the overlap, because the actor teleported past it. This class of bug is called tunneling, and it only shows up on slow machines and during stalls; which is to say, never on the developer’s machine.
The fix: decouple simulation from rendering
The standard solution, popularized by Glenn Fiedler’s “Fix Your Timestep!” article, is to stop letting the frame rate drive the simulation. Instead you run the simulation at a fixed rate. Mythic Maze uses 20 ticks per second, defined once in Simulation.TicksPerSecond and render as often as the display allows, which might be 60, 120, or 144 times per second.
These are now two independent clocks. The renderer runs as fast as it can. The simulation runs at exactly 20 Hz, no matter what. The job of the fixed-step clock is to sit between them and translate: given that 23 milliseconds of wall-clock time just passed, how many 50 ms simulation ticks do I owe?
The answer is usually zero or one, sometimes two, and the leftover time carries forward to the next frame. That carried-forward remainder is the accumulator, and it is the heart of this pattern.
What FixedStepClock actually does
The class has a deliberately small surface. You construct it with the frequency of whatever timestamp source you are using, then call Advance once per frame with the elapsed time:
private readonly FixedStepClock _clock = new(Stopwatch.Frequency);
private void OnAnimationFrame(TimeSpan _)
{
var now = Stopwatch.GetTimestamp();
var step = _clock.Advance(now - _lastTimestamp);
_lastTimestamp = now;
for (var i = 0; i < step.Ticks; i++)
{
_scene.Advance();
}
_frame = _scene.Snapshot(step.Alpha);
InvalidateVisual();
}
That is the entire integration. Note what the simulation receives nothing. _scene.Advance() takes no delta parameter at all. Every tick is identical, because a tick is always exactly 1/20th of a second in game time. There is no deltaSeconds to drift, and no way for a slow frame to produce an oversized step.
Advance returns a FixedStep record struct with three fields, and each one exists to answer a specific question.
The FixedStepClock class
Before breaking down those fields, here is the implementation in full. It is short enough to read in one sitting, and all of the frame-rate complexity in the game is concentrated in this class.
/// <param name="Ticks">Simulation ticks to run this frame.</param>
/// <param name="Alpha">Progress toward the next tick, 0 (inclusive) to 1 (exclusive), for interpolation.</param>
/// <param name="Dropped">True if a stall exceeded the catch-up cap and time was discarded.</param>
public readonly record struct FixedStep(int Ticks, float Alpha, bool Dropped);
/// <summary>
/// Fixed-timestep accumulator. The host feeds it elapsed time in its own timestamp units (e.g.
/// <c>Stopwatch</c> ticks); it answers how many simulation ticks to run. All arithmetic is
/// integral, so no time drifts. Catch-up is capped per frame; the excess is dropped after a stall.
/// </summary>
public sealed class FixedStepClock
{
/// <summary>Most ticks run in one frame before the rest of a stall is dropped.</summary>
public const int DefaultMaxTicksPerFrame = 4;
private readonly long _timestampFrequency;
private readonly int _ticksPerSecond;
private readonly int _maxTicksPerFrame;
// Measured in (timestamp units x ticks per second); one tick costs _timestampFrequency.
private long _accumulator;
public FixedStepClock(
long timestampFrequency,
int ticksPerSecond = Simulation.TicksPerSecond,
int maxTicksPerFrame = DefaultMaxTicksPerFrame)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(timestampFrequency);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(ticksPerSecond);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxTicksPerFrame);
_timestampFrequency = timestampFrequency;
_ticksPerSecond = ticksPerSecond;
_maxTicksPerFrame = maxTicksPerFrame;
}
public FixedStep Advance(long elapsedTimestamp)
{
_accumulator += Math.Max(0, elapsedTimestamp) * _ticksPerSecond;
var ticks = _accumulator / _timestampFrequency;
var dropped = ticks > _maxTicksPerFrame;
if (dropped)
{
ticks = _maxTicksPerFrame;
_accumulator %= _timestampFrequency;
}
else
{
_accumulator -= ticks * _timestampFrequency;
}
return new FixedStep((int)ticks, (float)_accumulator / _timestampFrequency, dropped);
}
/// <summary>Discards accumulated time, e.g. when resuming from pause.</summary>
public void Reset() => _accumulator = 0;
}
The state is a single long. The time calculation logic remains fairly easy. The sections below explain why each piece is designed that way.
Ticks: how many times to update
This is the main output, the number of simulation steps the caller owes. The caller runs that many iterations of its update loop, and the clock has already deducted them from its internal accumulator.
Alpha: how to draw between ticks
Here is the problem the fixed timestep creates. If the simulation only moves 20 times a second but you draw 60 times a second, then two out of every three frames show the exact same world state. Motion looks choppy, because you are showing 20 distinct positions per second.
Alpha solves this. It is the fraction of progress toward the next tick, from 0 (inclusive) to 1 (exclusive). If the accumulator is holding 30 ms of a 50 ms tick, alpha is 0.6. The renderer uses it to interpolate between the previous tick’s state and the current one, so an actor halfway through a move is drawn halfway between two tiles. The simulation stays discrete and reproducible; only the visuals are smoothed. In Mythic Maze this is what _scene.Snapshot(step.Alpha) is doing.
This is the key insight of the whole pattern: interpolation is a rendering concern, not a simulation concern. The game logic never knows about alpha.
Dropped: admitting you cannot catch up
Now consider a real stall. The player minimized the window for ten seconds, or the machine hibernated. The next Advance call receives ten seconds of elapsed time, which is 200 ticks.
Running 200 ticks in one frame would freeze the UI while the game fast-forwards through ten seconds of gameplay the player never saw. Worse if each tick is expensive, those 200 ticks might themselves take longer than the next frame budget, which generates even more debt on the following frame. The game spirals and never recovers.
FixedStepClock caps catch-up at DefaultMaxTicksPerFrame (4 ticks, or 200 ms of game time) and throws the rest away. The Dropped flag tells the caller this happened. Mythic Maze counts those events and surfaces them in the diagnostics panel, so a stall shows up as a number instead of as just a mysterious hitch.
Discarding time is a deliberate tradeoff. The alternative (replaying every missed tick) means the game is no longer running in real time, and the player watches a cutscene of events they missed. Dropping the excess keeps the game synchronized with the wall clock at the cost of some lost simulation time, which for a single-player puzzle game is fine.
The integer arithmetic detail
One implementation decision deserves a closer look. The accumulator is a long, not a double, and it is measured in an unusual unit: timestamp units multiplied by ticks per second.
_accumulator += Math.Max(0, elapsedTimestamp) * _ticksPerSecond;
var ticks = _accumulator / _timestampFrequency;
The obvious implementation would convert the elapsed timestamp to seconds, add it to a floating-point accumulator, and compare against a tick duration of 0.05 seconds. That reintroduces exactly the drift the fixed timestep was meant to eliminate, and the error compounds on every frame.
By scaling up front and keeping everything integral, one tick costs exactly _timestampFrequency units, division gives the whole tick count, and subtraction leaves an exact remainder. No value is ever rounded. A test in our test suite feeds the clock sixty frames of 16 or 17 timestamp units each – a deliberately uneven pattern totaling exactly 1000 units, or one second – and asserts that precisely 20 ticks come out. With a floating-point accumulator, that test will fail intermittently depending on the rounding.
Reset, and why pause needs it
Reset discards the accumulator. It exists for the stop/resume game case.
When a game is paused, or when the control is detached and reattached to the visual tree, wall-clock time keeps advancing while game time does not. Without intervention, the first Advance after resuming reports an enormous delta, which the clock would correctly interpret as a huge debt of ticks and which the catch-up cap would then partially drop. The result is a visible hitch on resume.
Calling Reset at the moment you resume declares that the gap was not game time and should not be accounted for. The game UI can then resume smoothly, and the clock will start accumulating ticks from zero again.
When you do not need this
To be fair about scope, not every game needs a fixed timestep. If your simulation is trivially cheap, has no collision detection, and has no reproducibility requirements (a particle toy, a screensaver, most simple UI animation) a variable timestep is simpler and fine.
The pattern is useful when any of these are true: you need deterministic replays or tests, you have discrete collision or grid movement that can tunnel, you plan to add networked play, or your game has a timer that players will compare against each other.
Summary
FixedStepClock is about 40 lines of code that converts “how much real time passed” into “how many simulation steps to run,” while handling three realities: the leftover fraction of a tick, the need to draw smoothly between discrete steps, and stalls long enough that catching up fully would make things worse.
The payoff is that our game loop can be a pure function of its input state. It takes no time parameter, so it cannot drift, cannot tunnel, and produces the same result on different monitor speeds. Every bit of frame-rate variability is taken care of by this one small class at the boundary, which leaves the game logic truly deterministic and testable.
Further reading
- Glenn Fiedler, “Fix Your Timestep!” – the canonical treatment of the accumulator pattern.
- Robert Nystrom, Game Programming Patterns, chapter on Game Loop – covers the variants and their tradeoffs.
- Microsoft, Stopwatch.Frequency – the timestamp source the clock is constructed with.
- Microsoft, Stopwatch.GetTimestamp – the per-frame reading fed into
Advance.