The reason we do fixed timestep updates in the first place is to ensure our realtime game simulations stay consistent.
Take a simple object falling under gravity of -9.8 m/s^2 under Euler integration, at 60 and 30 fps:
t @30fps vy py t @60fps vy py
------------------------------------------------------------
0.00 0.00 0.00 0.00 0.00 0.00
0.02 -0.16 0.00
0.03 -0.33 -0.01 0.03 -0.33 -0.01
0.05 -0.49 -0.02
0.07 -0.65 -0.03 0.07 -0.65 -0.03
0.08 -0.82 -0.04
0.10 -0.98 -0.07 0.10 -0.98 -0.06
0.12 -1.14 -0.08
0.13 -1.31 -0.11 0.13 -1.31 -0.10
0.15 -1.47 -0.12
0.17 -1.63 -0.16 0.17 -1.63 -0.15
0.18 -1.80 -0.18
0.20 -1.96 -0.23 0.20 -1.96 -0.21
0.22 -2.12 -0.25
0.23 -2.29 -0.30 0.23 -2.29 -0.29
0.25 -2.45 -0.33
0.27 -2.61 -0.39 0.27 -2.61 -0.37
0.28 -2.78 -0.42
0.30 -2.94 -0.49 0.30 -2.94 -0.47
0.32 -3.10 -0.52
0.33 -3.27 -0.60 0.33 -3.27 -0.57
0.35 -3.43 -0.63
0.37 -3.59 -0.72 0.37 -3.59 -0.69
0.38 -3.76 -0.75
0.40 -3.92 -0.85 0.40 -3.92 -0.82
0.42 -4.08 -0.88
0.43 -4.25 -0.99 0.43 -4.25 -0.96
0.45 -4.41 -1.03
0.47 -4.57 -1.14 0.47 -4.57 -1.11
0.48 -4.74 -1.18
0.50 -4.90 -1.31 0.50 -4.90 -1.27
So with only a single object moving for half a second, we already have a 3% relative error between our two framerates. In more complicated game systems, these differences can snowball, even when you compensate with more sophisticated integrators like RK4.
This happens because many common game systems and features - especially player input - are non-linear. The behaviour of the feature over a span of time isn't just its behaviour at the start of the interval times a duration, it arcs or jumps or changes in ways that are more difficult to model.
If we allow our game behaviour to shift based on its rate of updates, then players on different devices could get different (and possibly unfair) experiences, or the gameplay could change in the middle of an action if our framerate fluctuates. If we tune our game for 60 fps then need to drop to 30 on some devices (or tune if for 30 then want to upgrade to 60), we have to re-adjust all our tuning numbers to ensure it still feels the same.
Switching to a fixed timestep, as you've seen many gamedevs recommend, provides strong protection against these inconsistencies, by decoupling the simulation rate from the rendering framerate. Now whether we do one, two, more, or no simulation steps in a frame, each step's math comes out the same according to its sequence, rather than the particular instant it happened to run in this session.
If you add a variable-length update at the end, all those nice consistency guarantees go out the window, and we're back in the realm of potentially divergent gameplay depending on how fast our game renders in a particular context.
But maybe that doesn't matter - not every game will be adversely affected by this type of variability. If your game is turn-based, or works on discrete rather than continuous states (like tiles on a game board), then there are far fewer opportunities for your simulation to diverge based on how quickly or slowly it's updated - just watch for chains of dependency in your features, and cases where they can give different results if they get updated in a different order.
If that's the case though, and you have an update function that can scale to arbitrary timesteps, without caring about fixed timestep guarantees, then you might as well skip the loop entirely and just step forward in one go:
while (1) {
lag += Chronograph.getDelta();
update(lag);
render();
}
The only real reason to chop-up your update then is if you have some steps that become unstable, unreliable, or more costly over long timesteps, so you want to enforce a maximum step duration or break into sub-steps to avoid those issues specifically.
Lastly, if you want the game simulation consistency guarantees of a fixed timestep, plus the visual responsiveness of a variable timestep, you can have your cake and eat it too, like so:
while (1) {
lag += Chronograph.getDelta();
while (lag >= msPerTick) {
oldState = newState;
newState = update(oldState, msPerTick);
lag -= msPerTick;
}
renderState = interpolate(oldState, newState, lag/msPerTick);
renderState.applyVisualFeedback(lag);
render(renderState);
}
Here you interpolate smoothly between the last two states according to the "spare change" you have left after the fixed timestep loops. This ensures your motion is smooth and doesn't show a "beat frequency" or stutter due to mismatches between the rendering and update rates.
Then you can apply some just-in-time visual state updates, like animations, particle effects, etc. - stuff that doesn't affect the underlying rules of the game simulation, but are important to present to the player in a fresh state that accurately reflects the current moment.
By keeping these tweaks limited to a rendering-only state, we don't pollute the game simulation loop with inconsistencies based on the framerate, while still showing the player the freshest information available at the time of rendering.