1
\$\begingroup\$

Many suggest that a game loop looks something like this. See:

http://gameprogrammingpatterns.com/game-loop.html

http://www.koonsolo.com/news/dewitters-gameloop/

https://gafferongames.com/post/fix_your_timestep/

while (1) {
    lag += Chronograph.getDelta();
    while (lag >= msPerTick) {
        update(msPerTick);
        lag -= msPerTick;
    }
    render(lag);
}

However, is it equally viable to have a game loop as follows?

while (1) {
    lag += Chronograph.getDelta();
    while (lag >= msPerTick) {
        update(msPerTick);
        lag -= msPerTick;
    }
    update(lag); // 0 <= lag < msPerTick
    lag = 0;
    render();
}

I'm posing this question because my game should only be drawing sprites in the render call. Everything else should be handled by updates, particularly when sprites advance frames. In other words, the logic for advancing sprite frames should be handled by updates and rendering shouldn't have any affect on the state of my sprites.

\$\endgroup\$
  • \$\begingroup\$ It's viable in the sense that your code will compile and run. The question is: is it what you want? What problem are you trying to solve by making that change, or what set of features are you trying to support? (Hint: if determinism / consistent simulation is one of those features, then this change can introduce new risks) \$\endgroup\$ – DMGregory Nov 1 '17 at 23:48
  • \$\begingroup\$ So I'm working on a game where sprites have a delay between each image. For the sake of simplicity, suppose I have a sprite composed of 2 images and every 100ms, I alternate between them. Consider a scenario where lag=110ms, msPerTick=16ms. This leaves lag=14ms after all updates have been applied. The render function shouldn't be concerned with the logic of switching images; this logic is baked into the update function. So a call with update(14) will be applied before render is called. \$\endgroup\$ – goodpoutine Nov 2 '17 at 0:52
  • 1
    \$\begingroup\$ Not that this invalidates your question, but are you aware your third link (gafferongames) gives an answer to your own question? Unless I'm mistaken, your game loop in question is functionally the same as the game loop described under the heading Semi-fixed timestep. \$\endgroup\$ – Jibb Smart Nov 2 '17 at 3:36
1
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.