Smoothing Corrections to Client-Side Prediction?

Started by
8 comments, last by Angus Hollands 9 years, 8 months ago
I'm rebooting an old project on a multi-player top-down 2D space shooter using an authoritative server.

I have client-side prediction working (based on various articles like Gabriel Gambetta and gafferongames) as follows:

Every tick, the client...
1) Receives and applies snapshots from the server (potentially skipping old ones)
2) Collects input
3) Applies input locally and simulates
4) Logs that input as well as the ship's resulting position with a client timestamp C
5) Sends input to the server with timestamp C

Every tick (same rate as client), the server...
1) Receives and input from clients (potentially skipping some if the client is moving too fast)
2) Applies input and updates the world
3) Sends out snapshots to clients (including C)

At the client's step (1), when it gets snapshot S, that snapshot contains C. So the client rolls back through its logs to find the input that it sent marked C (trashing anything older), and finds the corresponding local position P for when that input was sent to the server. If S and P are far enough away from each other, it's time to correct. The naive way to do this, which works fine, is to set P to S (i.e. replace the position I had stored in the past with the position I just got from the server, also in the past), and re-simulate forward through all of my stored input, applying that input to the new updated position and updating the corresponding states in my input log with the updates ones. That works fine, here's how it looks with some ping and packet loss:

ForkedYawningIberianchiffchaff.gif

Blue is the predicted player position. Red is the received server position (S). Green is the stored position that we're rolling back to when we get the server position and correcting (P). The white dots represent all of the positions between green and blue, and those dots turn red when we did a correction this tick.

You can see jumps from the packet loss, but overall it's alright.

My problem comes from wanting to smooth those jumps. I got the bright idea that, rather than just setting P to S directly, I could do something like P += (S - P) * 0.3 -- ideally this would cause some softer adjustments. However, what this seems to cause is these sorts of cyclical adjustments in rapid succession, even when there's no input or anything that would cause the client to deviate from the server:

BrilliantMeatyAmericanbulldog.gif

All I changed in my code was making it a 30% snap instead of a 100% snap. The idea being that I'd rather slowly nudge the correction towards the right position than snap it outright. Is there something conceptually I'm missing here? Is there any other way to smooth these snaps that I could do instead? When I'm not hitting any keys at the end of the gif and the dots are still solid red, shouldn't the green ship slowly be getting dragged back towards the red? It seems to stay right in the middle of the blue and red ship, which is strange to me.

I have a hunch that the fact that my ships have a lot of momentum (as opposed to FPS games where your player stands still if you take your hands off the controls) could cause these repeating corrections, but I'm not sure.

Any advice would be greatly appreciated. Thanks!
Advertisement
What works pretty well is to do the following:

Let's assume nominal update rate is 100 ms per packet.
Each time you receive a position/velocity packet, calculate where the player will be 100 ms from now, based on that packet.
Then calculate what direction/velocity is needed from where you last rendered the player to make the player be rendered in that position in 100 milliseconds.
Then use that calculation as the position/velocity for the next 100 ms.

The EPIC library (Entity Position Interpolation Code) does this: http://www.mindcontrol.org/~hplus/epic/
enum Bool { True, False, FileNotFound };
A few questions:

1) How does that fold in with the normal input-based update for the player?
2) My updates are 20ms (give or take), so doing it in one update is still too fast for a smooth transition. If I spread it out over multiple frames, how do I keep from...
- (a) the adjustment getting out of date
- (b) the the client still thinking the player has the wrong position (because it does) and constantly re-correcting?

My updates are 20ms (give or take), so doing it in one update is still too fast for a smooth transition


First: 50 Hz network rate? That's pretty aggressive. Most players network ping will be significantly higher than 20 ms, and the packet overhead will be significant at that data rate. Are you sure you need a network rate this fast?

Second: Have you actually tried it? I don't think applying the delta over more than one network frame will be too bad, unless you have a really substantial network glitch, and if you do, nothing you do will feel right to the player. Better get back in "sync" as quickly as possible!

Third: How you update the player simulation based on input is actually entirely orthogonal to how you decide to display the players (both local and remote.)

If you want to apply a delta over time, then you have to model that explicitly. Keep a "delta amount" variable and a "delta time" variable. When you want to apply X amount of delta over time T, increment the delta by X and set the time-remaining to T. If there is another delta already being applied, that will then also be spread over the new interval -- you can "bake in" that by taking the current delta in effect when you calculate the new desired delta.
enum Bool { True, False, FileNotFound };

First: 50 Hz network rate? That's pretty aggressive. Most players network ping will be significantly higher than 20 ms, and the packet overhead will be significant at that data rate. Are you sure you need a network rate this fast?


Whoops. I knew that looked wrong when I saw it again. I meant 20Hz, not 20ms. I want to eventually do multiple physics ticks per snapshot/input message, say 2:1, but right now everything is just locked to a 20Hz heartbeat for simplicity's sake.

Second: Have you actually tried it? I don't think applying the delta over more than one network frame will be too bad, unless you have a really substantial network glitch, and if you do, nothing you do will feel right to the player. Better get back in "sync" as quickly as possible!


Right now, when I'm drawing a ship each frame (or really, setting the transform of the 3D model), I look at the ship's last in-world position and current in-world position, and just lerp between their X, Y, and angle. I'm going to switch to the v' = v + (dv * dt) approach though because I realize it also gives you extrapolation for free.

Anyway, because I'm doing this smoothing, the answer is yes, I have tried it. Here's what it looks like in practice:

DapperTatteredBalloonfish.gif

That's with 100ms ping and 3% PL each way simulated. It's still too jumpy for my liking, and I'd like to actually smooth it out over more than just one 20Hz tick.

For comparison (though it looks awful), here's what it looks like with 5Hz ticks rather than 20Hz ticks. The smoothing hangs at certain points and I'm not sure why, but I don't think it's noticeable at the faster tick rate. I just did this to prove to myself that it was, in fact, doing smoothing over the correction jumps.

OrangeAdoredCanary.gif

What I would really like to do is have different levels of adjustment. I'm fine with big jumps for big corrections, but I'd like a much softer way to do small adjustments and make them less noticeable by spreading them across multiple frames.

My nuclear option is to have a third layer of indirection between the player (blue) and the server (red) ship. Something like a smoothing ship that gets dragged along a little behind the player ship at all points, and every frame it cuts its distance between itself and the player ship by half or something, but that seems slushy and like a cop-out.
Just an update. I went ahead and switched from state tweening to doing the v' = v + (dv * dt) smoothing technique and it got rid of those hangs (thanks to extrapolation!). Here's some updated gifs, again at 100ms ping and 3% PL each way.

20Hz updates (no debug markers):

EnergeticBonyEidolonhelvum.gif

And 5Hz updates (now with debug markers):

GiftedBleakAfricanfisheagle.gif

I still think the corrections look too jumpy, but maybe I'm being pedantic. I still think a tiered approach would work really well though, but I just don't know how best to spread corrections out over multiple frames.

If you want to apply a delta over time, then you have to model that explicitly. Keep a "delta amount" variable and a "delta time" variable. When you want to apply X amount of delta over time T, increment the delta by X and set the time-remaining to T. If there is another delta already being applied, that will then also be spread over the new interval -- you can "bake in" that by taking the current delta in effect when you calculate the new desired delta.


Could you elaborate a little more here on what you mean by "baking in" the old delta when calculating the new? Is there a formula I could look at? Thanks!

Is there a formula I could look at?


Calculate the delta as "where you actually are displayed" (Pd) minus "where you would want to be" (Pt) and call it Pc (for correction.)
Set correction duration to Ct and start-of-correction-time St to "now."
Then, set the "where you actually area" position (Pa) to "where you would want to be" (Pt)
So, to receive a correction:

Pc <= Pd(t) - Pt
Ct = DURATION
St = t
Pa <= Pt
Now, as you update Pa with time advancing, you calculate the display position as follows:

if t > St + Ct: Pc <= 0
Pd(t) <= Pa(t) + Pc * (1 - (St + Ct - t) / Ct)
t is "now" in each of these.


Btw: Smoothing makes more sense as your display frame rate is higher than your network rate. Also, simulation rate should typically be higher than your display rate, or at least higher than your network rate.
The lower display is more what I'd expect the correct-over-one-tick-with-extrapolation to look like; the upper one is not what I was proposing you do.
enum Bool { True, False, FileNotFound };
Right. That makes sense. It's just linearly interpolating between the current position and desired position. What do you do if you get a correction while you're already applying one, though? Do you just throw out the old correction, or is there a way to get some sort of curve-style layering of corrections? How do you layer corrections while still ultimately guaranteeing convergence once the correction layers are finished?

Or am I just overthinking this? My movement is still too choppy at even just 100ms/3%PL each way and I'm not sure what I'm doing wrong. I'm getting a lot of corrections when I don't think I should be. The only thing I'm not doing is rolling back the server for input, but I feel like all that will do is cause snapping on everyone else's screen instead of just the controlling player's.

I wish I knew what Gimbal/Cosmic Rift/Ring Runner/Infantry/Subspace/Continuum do because they all tackle this momentum problem.

Btw: Smoothing makes more sense as your display frame rate is higher than your network rate. Also, simulation rate should typically be higher than your display rate, or at least higher than your network rate.
The lower display is more what I'd expect the correct-over-one-tick-with-extrapolation to look like; the upper one is not what I was proposing you do.


Do you mean the two gifs I posted? Currently the game both does one physics update and sends out screenshot at the same step at 20Hz with a 60Hz frame rate. The 5Hz update was just an example to show smoothing in action.

I can raise the simulation rate to 40Hz or 60Hz and send out snapshots every 2-3 sim ticks, but I don't think that's going to fix my problem with desync and corrections. If anything, it'll cause more problems because I'll need to send 2-3 input updates per client->server update, and then I'll definitely have to rollback on the server to retroactively apply old input (this will also be exaggerated error because of inaccuracies in estimating the true client-server time delta).

What do you do if you get a correction while you're already applying one, though?


The system I proposed updates the correction to say that you interpolate between what is currently displayed (which includes some part of correction for the last correction) and where you want to go. So, while you're "throwing away" the previous interpolation, you're using the result of that interpolation as a baseline for the new interpolation.

I can raise the simulation rate to 40Hz or 60Hz and send out snapshots every 2-3 sim ticks, but I don't think that's going to fix my problem with desync and corrections.


You will de-sync, because it's the internet, so nothing will fix that :-)

You really have two options: One is to display all remote entities in the past (once you're supposed to receive their commands,) which means they will have natural movements and corrections will be small, or you display all remote entities forward extrapolated to "now," which means that any correction, or just received commands, will generate significant jumping.
enum Bool { True, False, FileNotFound };

I recognise that this is an older post, but I feel there's something else to add.

For local players, the most important thing is input responsiveness. For remote players, something like EPIC would be a means of smoothing irregular updates due to packet loss. However, movement packets which are lost before the server receives them (in time) incur miscorrection on the server, for the next move (assuming that the lost packet contained changed inputs).

The only means that I can think to correct this is to send redundant previous moves to the server, (I.e 6 moves worth). If the server looses input for T 2, 3, 4 and receives moves for T5 +, it can use the last 3 moves to recover.

This will fix the stuttering corrections on the client, for this scenario.

This topic is closed to new replies.

Advertisement