Dealing with Latency...?

Started by
13 comments, last by John Schultz 18 years, 11 months ago
Quote:Original post by hplus0603
The problem with using that alpha is that, if you want to move from point 0 to point 1 with an alpha of 10 (say), you will move:

In the first timestep, from 0.0 to 0.1
In the second timestep, from 0.1 to 0.19
In the third timestep, from 0.19 to 0.271
...

I e, you'll start out going fast, and "ease off" towards the goal.

There are better interpolators/extrapolators pointed at from the Forum FAQ.


An IIR low-pass filter (LPF) does an excellent job of smoothing motion. The above analysis shows what will happen for two sample points. This will work fine even if an object moves then immediately stops. I find it can look better than ease-in and ease-out, especially for character motion.

For a constant stream of movement data, the IIR LPF will provide high-quality, smoothed motion (at the expense of added filter delay). If one's code results in jumpy motion, it sounds like the ghost is not being simulated with velocity. That is, it sounds like updates are showing up as descrete locations, where the interpolation is always towards a fixed location. If both the local, visual object and ghost object are moving (classic dead-reckoning), the interpolation will be smooth and seamless (as would be expected from an IIR LPF).

I looked through the FAQ, and found Gaffer's article on networked physics. The smooth() function from Cube.h:

Quote:
/// Smooth physics state towards target.

void smooth(const State &target, float tightness)
{
previous = current;
current = target;
current.position = previous.position + (target.position-previous.position) * tightness;
current.orientation = slerp(previous.orientation, target.orientation, tightness);
current.recalculate();
}


The tightness argument is alpha. This is the exact same IIR LPF I posted above. The demo works fine and shows very smooth motion (I also shipped a commercial game using the IIR LPF: the networked game was reviewed as smooth and having no lag (2002)). I tested various spline and other methods, but the simple IIR LPF always won the comparisons for that particular game (I also considered a predictive filter, such as the Kalman Filter (might help mitigate added filter delay), but since the IIR LPF worked so well, I did not pursue further research and testing). A combined IIR LPF + spline method might work better for some types of motion. Carefully changing alpha/tightness can help in reducing delay, but for a basic test and just getting started, it can be fixed.

What method in the FAQ do you see as having better performance?
Advertisement
The question is what you smooth towards. If you smooth towards a goal that evolves each frame, i e you smooth towards the dead-reckoned position of the interpolated object, then you'll have pretty good behavior. However, if you smooth towards a received target point, that stays the same until you receive the next target point, then you will not get the smoothest movement possible -- the single-pole filter you describe just isn't enough to clean up the stair-step signal of discrete jumps in position.

The OpenTNL whitepaper describes a forward extrapolation method where you interpolate between forward-extrapolated positions. The basic implementation of this (for a fixed timestep dt) would be when you receive an update for time T, you forward-extrapolate this update (using dead reckoning) to time T+dt. Then you take your last sample (arrived at T-dt, forward-extrapolated to T) and interpolate between these two points over a time interval of dt.
enum Bool { True, False, FileNotFound };
Quote:Original post by hplus0603
The question is what you smooth towards. If you smooth towards a goal that evolves each frame, i e you smooth towards the dead-reckoned position of the interpolated object, then you'll have pretty good behavior. However, if you smooth towards a received target point, that stays the same until you receive the next target point, then you will not get the smoothest movement possible -- the single-pole filter you describe just isn't enough to clean up the stair-step signal of discrete jumps in position.

The OpenTNL whitepaper describes a forward extrapolation method where you interpolate between forward-extrapolated positions. The basic implementation of this (for a fixed timestep dt) would be when you receive an update for time T, you forward-extrapolate this update (using dead reckoning) to time T+dt. Then you take your last sample (arrived at T-dt, forward-extrapolated to T) and interpolate between these two points over a time interval of dt.


How are you going to dead-reckon an object with an implied zero-velocity: "a received target point, that stays the same until you receive the next target point"?

Can you see that my example, Gaffer's example and demo, and the following from the OpenTNL site:

Quote:
In the Torque Game Engine, player objects controlled by other clients are simulated using both interpolation and extrapolation. When a player update is received from the server, the client extrapolates that position forward using the player's velocity and the sum of the time it will use to interpolate and the one-way message time from the server - essentially, the player interpolates to an extrapolated position. Once it has reached the extrapolated end point, the player will continue to extrapolate new positions until another update of the obect is received from the server.

By using interpolation and extrapolation, the client view can be made to reasonably, smoothly approximate the world of the server, but neither approach is sufficient for real-time objects that are directly controlled by player input. To solve this third, more difficult problem, client-side prediction is employed.


are all classic dead-reckoning (extrapolation) with interpolation between the locally simulated objects and their ghosts? I've implemented many different methods (including the one described on the OpenTNL site where RTT/2 is used to compute alpha), and the IIR LPF between local/ghost is the easiest general+jitter-free solution. You can simulate the "square-wave" motion case with Gaffer's demo: just tap a key to move the cube and watch the motion (server's view turned on): it starts fast and slows toward the target. This looks excellent for most object types (including characters: this is the same method I used in the past for an Xbox Live launch title). When alpha is computed from the RTT (to provide a more linear change in motion, but not smoother motion), extra work must be done to prevent jitter. Smoother start/stop/delta-v motion can be achieved by incorporating an interpolating spline (such as Catmull-Rom).

The most efficient way to handle an object "that stays the same until you receive the next target point" (velocity is known to be zero after target reached) is to send a reliable message telling the object to go to the new location, whereby the object moves to the new location using any preprogrammed/simulated motion behavior.
I guess my point is that you can do a better job if you receive position+velocity, instead of just position. As I said, I think the IIR method is much more reasonable if the target of the IIR is a position that, in turn, has been dead reckoned from a P+V update. If you don't have P+V, but you can delay by one update period, then you can extract P+V by using P(t-1) and P(t)-P(t-1) for your P+V at t.

The alternative is to be input-synchronous, and not "interpolate" anything at all. Instead, you co-simulate the same thing on each client. That's what we do, and it works very well (although it was a lot of effort to get to a point where it's robust, efficient and re-usable).
enum Bool { True, False, FileNotFound };
Quote:Original post by hplus0603
I guess my point is that you can do a better job if you receive position+velocity, instead of just position. As I said, I think the IIR method is much more reasonable if the target of the IIR is a position that, in turn, has been dead reckoned from a P+V update. If you don't have P+V, but you can delay by one update period, then you can extract P+V by using P(t-1) and P(t)-P(t-1) for your P+V at t.


I agree, and for slower, character-based motion, the above method of extracting/generating velocity (linear and angular) from position history can work fine (classic dead-reckoning typically requires sending the actual velocities). In cases where such implied velocities can be used, network bandwidth is saved. Additionally, velocity can be generated from known states (such as input position and known object behavior). In any case, as the examples above recommend, the velocities for the local and ghost objects should also be interpolated. Again, in the case of an autonomous object, all that needs to be sent is a moveto command: the object starts, accelerates, travels a predefined velocity, then stops at the target location, all predefined (and only requiring a small moveto command).

Quote:
The alternative is to be input-synchronous, and not "interpolate" anything at all. Instead, you co-simulate the same thing on each client. That's what we do, and it works very well (although it was a lot of effort to get to a point where it's robust, efficient and re-usable).


That's true, lock-step simulations require bit accurate calculations, and are very senstive (via divergence) if even a small error is present. My first networked-physics simulations were lock-step due to bandwidth restrictions (serial/modems: 1990-1994). I used CRC's on state blocks to determine divergence, and then resynchronized state. I remember early online FPS games of that genre that would allow the world to become out of sync, and wondered why they did not provide a means to resynchronize. I now use methods that are extremely fault tolerant while at the same time are relatively easy to develop and support.

This topic is closed to new replies.

Advertisement