Personally I've always hated that article. It's weirdly explained and a bit over-engineered. I think it'd be fun to do a little write-up here; you might not be looking for this since you asked for links, but I'll write for my own amusement (feel free to ignore).
In general you accumulate a delta-time based on the *previous* frame's duration. Something like this:
while (1)
{
// dt is the delta-time (change in time) over the previous `do_game`
float dt = get_timestep();
do_game(dt);
}
This is the typical "variable time-step" strategy. The significant takeaway is that each game simulation tick, represented by do_call, will use the time from the previous frame as an estimate of what to do now. Each time do_game is called a little bit different of a value is probably given for dt, hence the "variable" name, since dt varies each game loop.
The next strategy is to make sure the game will not run faster than some frequency, say, 60fps.
const float fps = 1.0f / 60.0f;
float accumulator = 0;
while (1)
{
float dt = get_timestep();
// limit the fps
accumulator += dt;
if (accumulator < fps)
continue;
accumulator = 0;
do_game(dt);
}
The game will run in a very tiny infinite loop waiting until 60fps has elapsed. This solution can break depending on how get_timestep is implemented. If the loop runs so fast that it returns a super tiny number (maybe even zero), the small floats may not add up accurately. Typically people try to fix this sort of thing by using more precision, i.e. changing the accumulator to be a double instead of a float -- this is just a bandaid solution without addressing the real problem.
The real problem is this loop does nearly nothing except calculating a delta time. This can cause another problem besides just breaking dt as the game now does not render at any faster than 60fps, and does not look for user-input at any time other than on 1/60s time intervals.
The loop can be adjusted slightly so user-input and rendering can occur outside of the do_game function:
const float fps = 1.0f / 60.0f;
float accumulator = 0;
while (1)
{
float dt = get_timestep();
// These two functions have been refactored out of the `do_game` function
// and placed here.
poll_input();
render_game();
// limit the fps
accumulator += dt;
if (accumulator < fps)
continue;
accumulator = 0;
do_game(dt);
}
It can be very nice to gather up all the user-input at a very high frequency, that way players can play very quickly and perform very complicated for fast maneuvers more freely. For example, fast and responsive input can be very important in many action-style games.
Now a question rises: "What is the point of rendering beyond 60fps"? Most of the time games are interpolating graphical effects over time. These interpolations can still potentially animate many times and be fully rendered and displayed on the screen multiple times between each call to do_game. For those unfamiliar with interpolation, tweening, or animation, here is a nice video about the topic.
However, in order to perform these kinds of interpolations some kind of time-based float need to be passed into the render function.
const float fps = 1.0f / 60.0f;
float accumulator = 0;
while (1)
{
float dt = get_timestep();
accumulator += dt;
int iterations = 0;
while (accumulator < fps)
{
++iterations;
accumulator -= fps;
}
float interpolant = accumulator;
poll_input();
do_game(dt);
render_game(dt);
}
Pay special attention to the interpolant. It is the leftover value, a value between 0 and fps. The code above is adding dt to accumulator, so accumulator should be non-zero.
The interpolant is what is used to render between frames. This should make some sense, as it is *usually* assigned a value between 0 and fps. In a typical game loop with no lag or really bad performance, the game will run quite quickly and go to the next game loop in much less time than fps. In these common cases iterations will be 0, and interpolant will be used.
In cases where the is running slowly, iterations will be non-zero. This is to account for times when the previous frame was, for example, 3 * fps + "a little more but less than a frame". In this instance iterations would be set to 3, and interpolant would be a small value between 0 and 1 frames. For now iterations and interpolant are unused, but will be revisited later.
There is a problem with the current strategy. If the game loop takes longer than fps, it is probably because the game is running slowly due to some un-optimized code (or because perhaps there was a performance spike, like when the operating system does something weird... looking at you Windows Updates). The next game loop will run, but only once. This essentially makes the game "pause" and the entire game will feel the hiccup.
One strategy is to use the iterations variable in a loop like so:
const float fps = 1.0f / 60.0f;
float accumulator = 0;
while (1)
{
float dt = get_timestep();
accumulator += dt;
int iterations = 0;
while (accumulator < fps)
{
++iterations;
accumulator -= fps;
}
while (iterations > 1)
{
poll_input();
do_game(fps);
--iterations;
}
float interpolant = accumulator;
poll_input();
do_game(dt, interpolant);
render_game(dt, interpolant);
}
Now the game loop can account for the occasional hiccup and simulate the game forward in the event iterations becomes non-zero. This can be quite nice to ensure the game logic itself keeps running forward through time. There lots of ways to tinker with this loop. For example, the iterations loop can have the input polling removed, as maybe it just isn't necessary for some games. Another strategy is to remove the iterations loop entirely, and do something like:
do_game(fps * iterations);
For some games this may be acceptable. The idea is to run the game an extra time, but with a very large time value. This can "jump" the game into the future to catch-up. For many cases this is not a good strategy, as sometimes big jumps can cause physics rigid bodies to tunnel, and many other kinds of nasty problems.
Another problem with the iteration loop, is if the previous game loop was really slow, the next one will probably be pretty slow too, as in the case of poorly optimized code or a slow machine. In this instance iterations will become larger and larger each game loop, forever trying to catch up the real-time. Very quickly this will devolve into a near-infinite loop, as iterations becomes ginormous.
A quick and easy fix is to clamp iterations below a small number, say, 5.
const float fps = 1.0f / 60.0f;
float accumulator = 0;
while (1)
{
float dt = get_timestep();
accumulator += dt;
int iterations = 0;
while (accumulator < fps)
{
++iterations;
accumulator -= fps;
}
// Prevent a death-spiral
if (iterations > 5)
iterations = 5;
float interpolant = accumulator;
while (iterations > 1)
{
poll_input();
do_game(fps);
--iterations;
}
poll_input();
do_game(dt, interpolant);
render_game(dt, interpolant);
do_game(dt);
}
Note that another way to phrase "non-variable timestep" is "fixed time-step", fixed as in constant.
Physics simulations typically need to run at a constant timestep. This is to make sure physics will be deterministic given the same machine, and same inputs to the game. Physics determinism is important for many games, and without it things like instant replays, save states, and other features can be impossible. Variable timesteps, when fed to physics, is a source of non-determinism, since each variation in time will cause *different* variations in floating point error for every single floating point operation that happens within the physics. The only practical solutions are to A) use only integers in physics, B) use floating-point determinism, C) use lots of little hacks and interpolations to "cover up" the effects (this strategy is only valid for multi-player games where the physics on the server can be deterministic, and I won't talk about it any more here since it is quite complicated).
In this article we cover the most practical solution for 95% of games: B.
Simply *only* run physics updates on a fixed (constant) frequency:
const float fps = 1.0f / 60.0f;
float accumulator = 0;
while (1)
{
float dt = get_timestep();
accumulator += dt;
int iterations = 0;
while (accumulator < fps)
{
++iterations;
accumulator -= fps;
}
// Prevent a death-spiral
if (iterations > 5)
iterations = 5;
float interpolant = accumulator;
float game_time = fps * iterations + interpolant;
while (iterations > 1)
{
do_physics();
--iterations;
}
poll_input();
render_game(dt, interpolant);
do_game(game_time);
}
Again, some games can get away with removing poll_input and do_game from inside the iterations loop, and instead only simulate physics here. There are many ways to tweak this loop, and this loop is just a typical example. In this example the game can get away with being called just once per main-loop, and physics is by its lonesome self in the iteration loop. This is the most common strategy for all games, and I would consider it "default".
To summarize so far, we have decoupled the update frequencies of input, physics, rendering, and the game by refactoring each topic into a different function. These different functions can be called at different frequencies by placing them inside/outside/before/after certain loops. Most of the time we want graphics to draw as fast as possible, physics to run at a fixed frequency, and the game to run just once per loop.
There is one last gotcha.
Since the rendering is happening at a different frequency than physics, and probably faster, there will be a very disruptive "jumpy glitchiness" to all physically simulated things as they render. This happens because as physics runs in discrete time-chunks, the nice and smooth interpolated rendering does not. The result is that each time physics runs it goes ahead by fps in time, essentially jumping. It works by running at fixed (constant) simulation cycles.
As the game renders it will render these positions happily. These positions are going to jump at *different times* than when the rendering geometry is displayed on screen.
Here are some solutions: A) only render the game when physics runs (move all functions, poll_input, do_game, render_game, inside the iteration loop); B) interpolate between two physics states.
Most modern games are going to be doing option B. The idea is to store the current and *previous* positions for all physics objects. Then when time comes to render, interpolate between the current and previous positions using the interpolant value. This does take up at least 2x memory to store a bunch of extra transforms, but hey, memory is sort of cheap nowadays and people really like smoothly interpolated physics stuff
At the beginning of this post I stressed the importance of using a previous value for the current loop. In this case, it is best to interpolate between a previous value of physics, and the current one. This will always introduce a one-frame-lag for physics objects. Nobody will notice. It will look smooth. It is impossible to interpolate from the current physics positions to the next one, since the next one has not yet been calculated! The next physics positions can be estimated, but this will cause some drastic visual artifacts when physics objects take sharp turns or change directions abruptly; don't do it. You have been warned.
Note: I didn't test any of this code, and there are probably bugs in there.