There's a long history of how we arrived at this common convention, with lots of fascinating challenges along the way, so I'll try to motivate it in stages:
1. Problem: Devices run at different speeds
Ever try to play an old DOS game on a modern PC, and it runs unplayably fast - just a blur?
A lot of old games had a very naive update loop - they'd collect input, update game state, and render as fast as the hardware would allow, without accounting for how much time had elapsed. Which means as soon as the hardware changes, the gameplay changes.
We generally want our players to have a consistent experience and game feel on a range of devices, (as long as they meet some minimum spec) whether they're using last year's phone or the newest model, a top-end gaming desktop or a mid-tier laptop.
In particular, for games that are competitive (either multiplayer or via leaderboards) we don't want players running on a particular device to have an advantage over others because they can run faster or have more time to react.
The surefire solution here is to lock the rate at which we do gameplay state updates. That way we can guarantee the results will always be the same.
2. So, why not just lock the framerate (eg. using VSync) and still run the gameplay state updates & rendering in lockstep?
This can work, but is not always palatable to the audience. There was a long time when running at a solid 30 fps was considered the gold standard for games. Now, players routinely expect 60 fps as the minimum bar, especially in multiplayer action games, and some older titles now look noticeably choppy as our expectations have changed. There's also a vocal group of PC players in particular who object to framerate locks at all. They paid a lot for their bleeding-edge hardware, and want to be able to use that computing muscle for the smoothest, highest-fidelity rendering it's capable of.
In VR in particular, framerate is king, and the standard keeps creeping up. Early in the recent resurgence of VR, games often ran around 60 fps. Now 90 is more standard, and harware like the PSVR is beginning to support 120. This may continue to rise yet. So, if a VR game limits its framerate to what's doable & accepted today, it's liable to be left behind as hardware and expectations develop further.
(As a rule, be wary when told "players can't perceive anything faster than XXX" as it's usually based on a particular type of "perception," like recognizing a frame in sequence. Perception of continuity of motion is generally far far more sensitive. )
The last issue here is that a game using a locked framerate also needs to be conservative - if you ever hit a moment in the game where you're updating & displaying an unusually high number of objects, you don't want to miss your frame deadline and cause a noticeable stutter or hitch. So you either need to set your content budgets low enough to leave headroom, or invest in more complicated dynamic quality adjustment features to avoid pegging the whole play experience to the worst-case performance on min-spec hardware.
This can be especially problematic if the performance problems show up late in development, when all your existing systems are built & tuned assuming a lockstep rendering framerate that now you can't always hit. Decoupling update & rendering rates gives more flexibility for dealing with performance variability.
3. Doesn't updating at a fixed timestep have the same problems as (2)?
I think this is the meat of the original question: if we decouple our updates and sometimes render two frames with no game state updates in between, then isn't it the same as lockstep rendering at a lower framerate, since there's no visible change on the screen?
There's actually several different ways games use the decoupling of these updates to good effect:
a) The update rate can be faster than the rendered framerate
As tyjkenn notes in another answer, physics in particular is often stepped at a higher frequency than the rendering, which helps minimize integration errors and give more accurate collisions. So, rather than having 0 or 1 updates between rendered frames you might have 5 or 10 or 50.
Now the player rendering at 120 fps can get 2 updates per frame, while the player on lower spec hardware rendering at 30 fps gets 8 updates per frame, and both their games run at the same gameplay-ticks-per-realtime-second speed. The better hardware makes it look smoother, but doesn't radically alter how the gameplay works.
There's a risk here that, if the update rate is mismatched to the framerate, you can get a "beat frequency" between the two. Eg. most frames we have enough time for 4 game state updates and a little leftover, then every so often we have enough saved up to do 5 updates in a frame, making a little jump or stutter in the movement. This can be addressed by...
b) Interpolating (or extrapolating) the game state between updates
Here we'll often let the game state live one fixed timestep in the future, and store enough information from the 2 most recent states that we can render an arbitrary point between them. Then when we're ready to show a new frame on-screen, we blend to the appropriate moment for display purposes only (ie. we don't modify the underlying gameplay state here)
When done right this makes the movement feel buttery smooth, and even helps to mask some fluctuation in framerate, as long as we don't drop too low.
c) Adding smoothness to non-gameplay-state changes
Even without interpolating gameplay state, we can still get some smoothness wins.
Purely visual changes like character animation, particle systems or VFX, and user interface elements like HUD, often update separately from the gameplay state's fixed timestep. This means if we're ticking our gameplay state multiple times per frame, we're not paying their cost with every tick - only on the final render pass. Instead, we scale the playback speed of these effects to match the length of the frame, so they play as smoothly as the rendering framerate allows, without impacting game speed or fairness as discussed in (1).
Camera movement can do this too - especially in VR, we'll sometimes show the same frame more than once but reproject it to take into account the player's head movement in between, so we can improve the perceived latency and comfort, even if we can't natively render everything that fast. Some game streaming systems (where the game is running on a server and the player runs only a thin client) use a version of this too.
4. Why not just use that (c) style for everything? If it works for animation and UI, can't we simply scale our gameplay state updates to match the current framerate?
Yes* this is possible, but no it's not simple.
This answer is already a bit long so I won't go into all the gory details, just a quick summary:
Multiplying by deltaTime
works to adjust to variable-length updates for linear change (eg. movement with constant velocity, countdown of a timer, or progress along an animation timeline)
Unfortunately, many aspects of games are non-linear. Even something as simple as gravity demands more sophisticated integration techniques or higher-resolution substeps to avoid diverging results under varying framerates. Player input and control is itself a huge source of non-linearity.
In particular, the results of discrete collision detection and resolution depend on update rate, leading to tunneling and jittering errors if frames get too long. So a variable framerate forces us to use more complex/expensive continuous collision detection methods on more of our content, or tolerate variability in our physics. Even continuous collision detection runs into challenges when objects move in arcs, requiring shorter timesteps...
So, in the general case for a game of medium complexity, maintaining consistent behaviour & fairness entirely through deltaTime
scaling is somewhere between very difficult & maintenance intensive to outright infeasible.
Standardizing an update rate lets us guarantee more consistent behaviour across a range of conditions, often with simpler code.
Keeping that update rate decoupled from rendering gives us flexibility to control the smoothness and performance of the experience without altering the gameplay logic.
Even then we never get truly "perfect" framerate independence but like so many approaches in games it gives us a controllable method to dial in toward "good enough" for the needs of a given game. That's why it's commonly taught as a useful starting point.
while (isTimeForUpdate)
, notif (isTimeForUpdate)
. The main goal isn't torender()
when there hasn't been anupdate()
, but toupdate()
repeatedly in betweenrender()
s. Regardless, both situations have valid uses. The former would be valid if state can change outside of yourupdate
function, e.g., change what is rendered based on implicit state such as the current time. The latter is valid because it gives your physics engine the possibility to do a lot of small, precise updates, which e.g. reduces the chance of 'warping' through obstacles. \$\endgroup\$