3
\$\begingroup\$

A highly recommended game loop is one from an article called Fix Your Timestep. It presents a method to update using a semi-fixed time step with interpolated rendering.

The problem with this is that, by interpolating the rendering, you are effectively averaging two states in the game - the current and the previous. If the previous frame is averaged with the current frame, the player will be shown the game slightly delayed.

If the player is stationary in one update, then teleports in the next, the interpolation will show a state where the player is mid-way between a teleport very briefly.

For time-critical games, is my thinking correct that the player may see a different/slightly lagged state on screen (the interpolated state) than what is actually stored in memory?

The only fix I can think of is to interpolate the current state + the next, not yet calculated, state. This gives the correct image on screen instead of a slightly delayed previous image... but then we are stuck in a paradox of calculating the next future update.

Or perhaps the time difference is so minimal that it doesn't matter?

I am referring to the following line, the current state is where we are at this moment in time and not fully displayed:

State state = currentState * alpha + previousState * ( 1.0 - alpha );
\$\endgroup\$

2 Answers 2

5
\$\begingroup\$

Firstly, on the subject of teleportation: most interpolation systems will simply disable interpolation for an object on a frame where it's teleported.

You can see this for example in Unity, when using the interpolation options on the Rigidbody and Rigidbody2D components. If you move them solely with physics - forces, velocities, torques, MovePosition() etc. - then their display position and orientation is interpolated between the two most recent physics steps.

But if you set their position or rotation via the Transform component, the engine disables the interpolation for the object on this frame. Under the hood this could be done by setting a flag, or by overwriting both the current and previous physics step outputs with the teleported position/orientation. The position and orientation you see on the next display frame will be the fresh values from the Transform, and rigid body simulation and interpolation will resume next frame.

So, in a well-designed system, you don't need to worry about teleporting objects showing momentary "ghosts" between their previous and new positions due to physics interpolation. You can simply turn it off when it's not valid.


I think the thornier concern here though is this: "If I interpolate with a previous simulation state, isn't my displayed state 'old' or 'lagged' behind the true game state?" - ie. are we introducing unwanted latency into our game by this method?

Let's start by bounding the maximum latency we can introduce this way.

The worst case is when our rendered frame falls just after the "previousState" timestamp, and alpha is very close to zero. At this point we're one full frame in the past, 20ms if we're stepping our game simulation 50 times per second. That's already at the low end compared to typical latencies in realtime online games, where similar techniques are used to transition smoothly between discrete updates from the server/host/peers. Response times of 80-100ms can still be perceived as instantaneous, so we're already in fairly safe territory.

But we can do better. :) When the current frame falls just before the "currentState" timestamp, alpha is very close to one and our latency is practically nothing! (If we're only counting latency due to the fixed timestep - input processing and display latencies still add on top of this)

In practice we'll vary between these extremes with no particular bias. So overall, at 50 ticks per second, we're displaying a state that's an average of 10 ms old. For most purposes, this is below the threshold for visual perception of synchronicity, and closing in on the threshold for synchronous audio perception too.

And we can still do better! Because not all game feedback is tied to the fixed timestep. When the player presses a button to fire their weapon, we can spawn the muzzle flash and play the shooting sound immediately in the same frame. Depending on when we are in our interpolated time span, the projectile (assuming it's a literal projectile and not a hitscan raycast) might be rendered closer or further from the barrel, but either way the player gets immediate feedback at the very earliest opportunity we could offer - even if our updates were in lockstep. Similarly, if the player presses jump, we can immediately start blending the character into their jump animation, even if it will take another physics step or two before the upward motion becomes apparent in their physics. UI changes too, like hit markers on a reticle, can react instantly according to the rendered frame time, keeping perceived latency as low as it can be at our rendering framerate.

This helps highlight an important distinction here: how does a player know when data is "old"? Yes, technically that box moving at 1 m/s is now at (0.01, 0, 0), since its most recent calculated state is 10 ms ahead of the frame we rendered with it at (0, 0, 0). But from the player's point of view, it's moved exactly 0.0166 m since the last rendered frame 16.66 ms ago, so it's exactly where they expect to see it. The only yardstick the player can use to judge what's "now" is how the game reacts to their own input, and as we've seen above a well-designed feedback system can begin to react immediately, without adding frames of latency. Beyond that it's second-order consequences of game systems reacting to the effects of player actions, or the consequences of players' reactions in turn, all of which gets swamped by the round trip time of perception to action, which is typically a factor of 10 slower than the latencies we're considering. So while the current state might be old in terms of the pure math of the underlying system, from a player experience standpoint it's often not appreciably different from realtime.

So, tl;dr: At a decent framerate & fixed timestep, the latency introduced by decoupling your game updates is minor and very manageable for most game types.


The idea of blending with "the next, not yet calculated, state" is a related technique called extrapolation. This has somewhat the opposite trade-offs: simple, linear motion is represented precisely, but things like collision reactions appear a full frame late (so you might observe two objects touch or even interpenetrate before any corresponding collision effects or sounds trigger). Complex motion can also appear jagged, as the naive prediction keeps overshooting & correcting next frame. Generally these artifacts are easier to perceive than the slight latency of a single frame of interpolation, so the main reason to go this route is for simplicity (since you can skip storing two steps' worth of transform data)

\$\endgroup\$
2
\$\begingroup\$

A small clarification, in addition to DMGregorys excellent considerations:

The article Fix Your Timestep by Gaffer on Games discusses physics updates only.

The article proposes to use multiple physics steps vithin a single visual step. This could be an API call like:

SimulateFixedStep(VerySmallButFixedStepTime, FlexibleFrameTime)
 // Eg. SimulateFixedStep(0.005, 0.01666666667) <- we seem to be at 60 fps atm
 // NB. 0.005 fits 3.333333334 times in 0.01666666667

When executing the call above, the physics calculator (may it be built-in 3rd party, or own code) would execute 3 iterations, "consuming" 0.015 seconds in total. It would then store (accumulate) the remaining 0.00166666667 seconds for the next frame (note that this number has a zero more!).

This might produce visual stutter, as the remainder changes. Big sometimes, small sometimes.

As an alternative, the article proposes to consume the entire frame time, by running a last calculation with the remaining (very very small) time:

0.005 secs
0.005 secs
0.005 secs
0.00166666667 secs <- could be anything between 0 and 0.005 s
------------------
0.01666666667 secs consumed, ie. the entire time of measured previous cycle (up to measure point in the code)

The article dismisses this way to do it, by claiming that the physics calculation may become unstable if the remainder is very small, like 0.00001 s. May be so - depends on the internals. Hard to say.

As the eventual, best solution, the article proposes to pre-calculate one additional physics result, ahead of time, by maintaining the VerySmallButFixedStepTime of 0.005 seconds. Then it proposes to linearly interpolate between the two last results:

0.005 secs
0.005 secs
0.005 secs <- 2nd last result (or "state)
lerp here, that's what the next visual frame uses
0.005 secs <- last result (and this is already ahead of time)

Ie. the final result (your "state") is lerp'ed from between 2nd last and last. This is by all means a good way to do it, but modern physics engines should have features like this built-in. Interpolatng between two states is a job too - takes time as well! :-) Id really propose to see what the built-in physics engine can do! If this is NOT about physics, then different rules apply.

But this is not a matter of showing out-of-sync, rather one of showing as-sync-as-ever-possible. To then sneakily/quickly perform tasks that aren't hard-bound to core physics calculation may be a good idea, as proposed by DMGregory.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Note that game physics in this sense isn't limited to rigidbody simulations provided by a physics engine. You can get consistency benefits by putting other game logic on a fixed step too - anything gameplay critical like damage handling, etc. That way the gameplay consequences of a given sequence of actions are always consistent, and you don't get variability depending on the framerate. This can be important for fairness in competitive games, because scaling effects by deltaTime is not always sufficient to get stable & consistent results. \$\endgroup\$
    – DMGregory
    Sep 2, 2017 at 13:58
  • \$\begingroup\$ True indeed, there are multiple heartbeats. And hickups :-). \$\endgroup\$
    – Stormwind
    Sep 2, 2017 at 15:35

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .