Somewhat jerky movement when doing client side prediction

Started by
20 comments, last by All8Up 12 years, 7 months ago
So, been hacking away at my multiplayer client/server over the weekend, based a lot on what AllEightUp and hplus0603 helped me with in this thread (thanks again, both of you if you read this post also). I've run into a bit of a problem though, first let me describe my setup:

  • Client runs physics simulation in 15ms ticks (66.66Hz)
  • Client sends commands to server every two ticks (33.33Hz), clients send commands to the client in groups of two
  • Server runs its own physics simulation at 15ms ticks also
  • Server sends result back to clients every 45ms (22.22Hz)

This is my general setup, and it's working pretty well (even got a decent implementation of entity smoothing and lag compensation working), however I'm having problems with the client input prediction on a client that owns an entity. It goes like this:

  1. Client physics step checks input and register a FORWARD movement
  2. The client predicts where this FORWARD movement will take it, and puts it in the queue of yet-to-be.verified commands and sends it to the server
  3. Here's the problem: I now need to move the client character on the owning machine directly, and this is where I run into problems. I'll explain below.

So, I now have a queued up command from the physics step that says I need to move forward, it has been sent to the server and everything is dandy. But I just can't get the movement on the client itself to be completely smooth. One physics step is 15ms, so I tried to smooth the movement command generated in the physics step over the next X frames. I also tried to do the on-screen movement itself on in the physics step also by just moving the client character forward as much as the movement command generated, as I figured that the physics simulation is running at almost 67 fps it would be enough to not be noticeable.

I want to say that both of the approaches I've mentioned (smooth the movement over the next X frames, up until 15ms has passed and also doing the movement in the physics step) works decently well, and if I wasn't such a perfectionist I probably would not have noticed that it's a tiny tiny tiny bit jerky compared to doing something like "position += movement * delta" which creates the ultimate smooth movement. So I suspect what I'm looking for his some hints, pseudo code (or real implementation if anyone got) of how to get that really really smooth movement on the client that you're doing the input prediction on.


Edit for clarification: This is not "jerkyness" coming from corrections by the server, etc. that is handled all well and good, this is jerky movement on the character itself when moving on the commands that have yet to be verified and returned by the server. Basically the predicted movement, which is supposed to be completely smooth but I just can't get it so.


To explain further, I'm looking to get the exact same smooth movement as "position += movement * delta" would get me if I was making a single player game and put that code in the update function that gets executed once every frame. But now I'm checking the input in the fixed update / physics step so it's synced to the servers 15ms tick rate but then when I'm going to move/draw the character in update based on this, the character jerks a tiny tiny tiny tiny bit.
Advertisement
I'm going to try to expand om my previous post to give a clearer example of what I want, again assume this:
  • Clients physics step / fixed update runs at 66.66Hz (15 ms)
  • Clients update/framerate runs as fast as possible
  • I generated input commands in the physics step
  • I need to move the on screen character in the normal frame based update function

In pseudo code it it would look something like this:



// 66.66Hz, or 15ms runtime
void physicsStep() {
byte keystate = 0;

if(keyIsDown(w)) keystate |= 1;
if(keyIsDown(s)) keystate |= 2;
if(keyIsDown(a)) keystate |= 4;
if(keyIsDown(d)) keystate |= 8;

queueStateForSendingToServer(keystate);
queueStateForLocalUpdate(keystate);
}


// normal update, runs as fast as possible based on frame rate
void update() {
if(anyStatesInLocalQueue) {
// this is where I have the problem
// I'm not sure of how to apply the
// states generated in physicsStep()
// in the frame based update function
// to achieve a smooth movement
}
}


Again, to further elaborate: When I come into the update() function, which will run one or many times in a row depending on my frame rate I have the commands/states locally that have not been applied to the character yet, which is what I need to do, smoothly. I've tried several techniques, for example:

* Take the next state, interpolate over it from 0-15ms, then take the next one, etc. It doesn't seem to achieve smooth results.
* Calculating the positions before hand and smoothing between them, still doesn't give me 100% correct results.

I've also tried things like calculating the expected position from a specific movement in the physicsStep and then smoothing from current position to expected position in update, like this:


Vector3 lerpFrom;
Vector3 lerpTo;
float lerpTime ;

void physicsStep() {
lerpTime = currentTimeSinceGameStarted;
lerpFrom = currentPosition;
lerpTo = currentPosition + currentCalculatedInputVector;
}

void update() {
currentPosition = Vector3.Lerp(lerpFrom, lerpTo, currentTimeSinceGameStarted - lerpTime / 0.015f);
}


But no luck with this approach either. And again, for emphasis: It's not insanely jerky, but it's not as smooth as when doing a single player game and doing the movement in the update function with just "currentPosition += movementVector * frameDelta * 50.0f;*


Vector3 lerpFrom;
Vector3 lerpTo;
float lerpTime ;

void physicsStep() {
lerpTime = currentTimeSinceGameStarted;
lerpFrom = currentPosition;
lerpTo = currentPosition + currentCalculatedInputVector;
}

void update() {
currentPosition = Vector3.Lerp(lerpFrom, lerpTo, currentTimeSinceGameStarted - lerpTime / 0.015f);
}


But no luck with this approach either. And again, for emphasis: It's not insanely jerky, but it's not as smooth as when doing a single player game and doing the movement in the update function with just "currentPosition += movementVector * frameDelta * 50.0f;*


My first thought here is that floating point imprecision could be causing problems. Given the standard lerp implementation of "a' = a + (b-a) * t" and the way you are calculating t, there are a couple places you could be hitting problems with precision. I don't think this is the most likely case but you might want to log out the from/to and the time percentage and see what the values are. If you see numbers like 1000.0001f for a position and a number like 0.0001f for the percentage then you are likely hitting problems in this area. The fix, if that is a problem, is to expand out the entire equation and rearrange things to not perform the division until the last possible point. (Division is usually the worst culprit of floating point error.)

Another variation along the same lines is that the timer code is not accurate enough and you are getting inconsistent deltas. Variations of this include having your time sampling done in the wrong locations such that they are not compensating for varying loop times. If the call to physics is part of the main loop, each time it hits it could be throwing off the delta time considerably such that your time stream is 2ms, 2ms, 2ms, 5ms, 2ms, 2ms, 2ms, 5ms, etc. Each time you hit 5ms your interpolation could/would "jump" considerably.

Finally, why are you lerping at all? My thought on this is to go back to the standard "a' = a + v * delta" and simply recalculate v to point at your target point as it changes from physics/network update. There are some downfalls to this but in general the upside is that your game loop code is written without anything unusual, you get interpolation/extrapolation for free (on a linear path at least) and to perform smoothing/correction you simply modify the v vector smoothly instead of fiddling with absolute start/end points. Fiddling with the v vector to do smoothing and compensation is not the most intuitive item in the world but it is generally easier than doing the lerps all over the place since you can localize the code to one place (generally after physics/network but before the client presentation code).

Mostly the trick to using the velocity item is if the velocity is to be open ended or targeted. What I mean is that if you don't get an updated location and you go past the target point, do you keep extrapolating down the path or go ahead and stop? Sometimes "stop" is the correct answer, i.e. things on fixed paths, mostly not though. The way I did this last time was using a spring system, I let the motion continue past the target but started slowing down until an update arrives. With very bad packet loss and/or variable latencies, this acted as a smoothing system where the objects were not rock solid continual speeds and such but they tended to adjust automatically to the bad network conditions without a lot of code involved. Under good network conditions though, the slight rubber band effect is imperceptible. Keep in mind that this is just for the client presentation, proper latency hiding and all that are done at higher levels, this just helps out and maintains the code in a manner which is not so different than single player code.

[quote name='fholm' timestamp='1314000735' post='4852174']

Vector3 lerpFrom;
Vector3 lerpTo;
float lerpTime ;

void physicsStep() {
lerpTime = currentTimeSinceGameStarted;
lerpFrom = currentPosition;
lerpTo = currentPosition + currentCalculatedInputVector;
}

void update() {
currentPosition = Vector3.Lerp(lerpFrom, lerpTo, currentTimeSinceGameStarted - lerpTime / 0.015f);
}


But no luck with this approach either. And again, for emphasis: It's not insanely jerky, but it's not as smooth as when doing a single player game and doing the movement in the update function with just "currentPosition += movementVector * frameDelta * 50.0f;*


My first thought here is that floating point imprecision could be causing problems. Given the standard lerp implementation of "a' = a + (b-a) * t" and the way you are calculating t, there are a couple places you could be hitting problems with precision. I don't think this is the most likely case but you might want to log out the from/to and the time percentage and see what the values are. If you see numbers like 1000.0001f for a position and a number like 0.0001f for the percentage then you are likely hitting problems in this area. The fix, if that is a problem, is to expand out the entire equation and rearrange things to not perform the division until the last possible point. (Division is usually the worst culprit of floating point error.)

Another variation along the same lines is that the timer code is not accurate enough and you are getting inconsistent deltas. Variations of this include having your time sampling done in the wrong locations such that they are not compensating for varying loop times. If the call to physics is part of the main loop, each time it hits it could be throwing off the delta time considerably such that your time stream is 2ms, 2ms, 2ms, 5ms, 2ms, 2ms, 2ms, 5ms, etc. Each time you hit 5ms your interpolation could/would "jump" considerably.

Finally, why are you lerping at all? My thought on this is to go back to the standard "a' = a + v * delta" and simply recalculate v to point at your target point as it changes from physics/network update. There are some downfalls to this but in general the upside is that your game loop code is written without anything unusual, you get interpolation/extrapolation for free (on a linear path at least) and to perform smoothing/correction you simply modify the v vector smoothly instead of fiddling with absolute start/end points. Fiddling with the v vector to do smoothing and compensation is not the most intuitive item in the world but it is generally easier than doing the lerps all over the place since you can localize the code to one place (generally after physics/network but before the client presentation code).

Mostly the trick to using the velocity item is if the velocity is to be open ended or targeted. What I mean is that if you don't get an updated location and you go past the target point, do you keep extrapolating down the path or go ahead and stop? Sometimes "stop" is the correct answer, i.e. things on fixed paths, mostly not though. The way I did this last time was using a spring system, I let the motion continue past the target but started slowing down until an update arrives. With very bad packet loss and/or variable latencies, this acted as a smoothing system where the objects were not rock solid continual speeds and such but they tended to adjust automatically to the bad network conditions without a lot of code involved. Under good network conditions though, the slight rubber band effect is imperceptible. Keep in mind that this is just for the client presentation, proper latency hiding and all that are done at higher levels, this just helps out and maintains the code in a manner which is not so different than single player code.
[/quote]

Hey, and thanks again for an excellent answer, I owe you a lot!

I've been doing some digging and what you've described here, is what's happening I think:

[quote=AllEightUp]If the call to physics is part of the main loop, each time it hits it could be throwing off the delta time considerably such that your time stream is 2ms, 2ms, 2ms, 5ms, 2ms, 2ms, 2ms, 5ms, etc. Each time you hit 5ms your interpolation could/would "jump" considerably.[/quote]

It's not exactly the same, but basically the t i use for input to my lerp in the update function never reaches 1.0, so when I go to the physics step there is some movement that still hasn't happened on screen since it only got to 0.8 or 0.75, etc. and then the new physics step will happen and it will jump the position forward and start lerping towards the next one, etc.

I've also learned that using Unity (which is what I am), you shouldn't read input in the physics step (FixedUpdate in unity) as the input state is only refreshed once every frame, which could also be part of my problem. And that using Unity you really should be reading input inside of your Update function, so I did work on a solution that did the input reading and quantification to 15ms steps inside my Update function, and it seems to be working properly.

Basically what I did with the Update based version (instead of FixedUpdate/physics step) was that quantify the commands to 15ms by using delta time and counting upwards in a variable to 15ms and using that to calculate movement and creating commands that need to go to the server, this is how it looks without the server parts:




byte pstate;
float ptime;
Vector3 pvector;
Vector3 cpos;

void MoveCmd(float delta) {
var state = GetKeyState();

// Most uncommon path
if(pstate != state) {
cpos += pvector;
ptime = 0;
pvector = Vector3.zero;

// Most common path
} else if(ptime+delta >= 0.015f) {
delta -= (0.015f-ptime);
cpos += KeyStateToVector(state);
pvector = Vector3.zero;
ptime = 0;
}

while (delta >= 0.015f) {
cpos += KeyStateToVector(state);
delta -= 0.015f;
}

ptime += delta;
pstate = state;
pvector = KeyStateToVector(state) * (ptime / 0.015f);

transform.position = cpos + pvector;
}



It does give me silky smooth movement and seems to be working on both the client and server properly (this is just the client part), i'm just not sure "it's right" (the correct way to do it)? But I saw now other way, especially since unity doesn't update the Input between physics step (only once per frame).

Also one questions, about: [color="#1C2837"]a' = a + v * delta
[color="#1C2837"]

[color="#1C2837"]How would an implementation of this look, I'm assuming something like:

  • [color="#1c2837"]a = current position
  • [color="#1c2837"]a' = new position
  • [color="#1c2837"]v = vector from a to valid position
  • [color="#1c2837"]delta = frame delta


[color="#1c2837"]so in the physics step I would set something like:

[color="#1c2837"]p = serverposition + allUnverifiedCommandsReapplied
v = p - a // from a to p





[color="#1c2837"]and then in the update/frame step I would do something like this:


[color="#1c2837"]a' = a + (v * delta)
a = 'a



[color="#1c2837"]but wouldn't this keep me moving "over" my p ? maybe I'm missunderstanding.

[color="#1c2837"]Again, thanks for all your answers and the time you've taken out of your day to help me!
Ok, so I tried implementing the 'a = a + (v * delta); stuff but I just can't seem to get it right, here's the code (only client side, no network integration now just making sure the smoothing works properly):



Vector3 tpos; // the position we want to get to
Vector3 tvelocity; // the direction from the current position to the one we're going to

// Init code, just run once when code gets activated first
void Start () {
tpos = transform.position;
tvelocity = Vector3.zero;
}

// Physics step
void FixedUpdate() {
// KeyStateToVector and just return a vector from a keystate, which is returned
// from GetKeyState which returns a byte with flags set depending on keys down
tpos += KeyStateToVector(GetKeyState());
tvelocity = (tpos - transform.position).normalized;
}

// Every frame
void Update () {
transform.position = transform.position + (tvelocity * Time.deltaTime * 5.0f);
}



Maybe I misunderstood something from your implementation, but I get a very "springy"-feel to it when I move and it's not possible to make twitch reactions like changing direction, etc. There's also some problems with overshooting and stuff but that can be fixed by clamping the transform.position.

Ok, so I tried implementing the 'a = a + (v * delta); stuff but I just can't seem to get it right, here's the code (only client side, no network integration now just making sure the smoothing works properly):



Vector3 tpos; // the position we want to get to
Vector3 tvelocity; // the direction from the current position to the one we're going to

// Init code, just run once when code gets activated first
void Start () {
tpos = transform.position;
tvelocity = Vector3.zero;
}

// Physics step
void FixedUpdate() {
// KeyStateToVector and just return a vector from a keystate, which is returned
// from GetKeyState which returns a byte with flags set depending on keys down
tpos += KeyStateToVector(GetKeyState());
tvelocity = (tpos - transform.position).normalized;
}

// Every frame
void Update () {
transform.position = transform.position + (tvelocity * Time.deltaTime * 5.0f);
}



Maybe I misunderstood something from your implementation, but I get a very "springy"-feel to it when I move and it's not possible to make twitch reactions like changing direction, etc. There's also some problems with overshooting and stuff but that can be fixed by clamping the transform.position.


You can't normalize the tvelocity in the above or you loose the approximation of "velocity" and only get direction. Otherwise, it looks like everything is correct though I'm still curious about the "* 5.0f" unless somehow deltaTime is not in seconds.

[quote name='AllEightUp' timestamp='1314015637' post='4852244']
My first thought here is that floating point imprecision could be causing problems. Given the standard lerp implementation of "a' = a + (b-a) * t" and the way you are calculating t, there are a couple places you could be hitting problems with precision. I don't think this is the most likely case but you might want to log out the from/to and the time percentage and see what the values are. If you see numbers like 1000.0001f for a position and a number like 0.0001f for the percentage then you are likely hitting problems in this area. The fix, if that is a problem, is to expand out the entire equation and rearrange things to not perform the division until the last possible point. (Division is usually the worst culprit of floating point error.)


Hey, and thanks again for an excellent answer, I owe you a lot!

I've been doing some digging and what you've described here, is what's happening I think:

[quote=AllEightUp]If the call to physics is part of the main loop, each time it hits it could be throwing off the delta time considerably such that your time stream is 2ms, 2ms, 2ms, 5ms, 2ms, 2ms, 2ms, 5ms, etc. Each time you hit 5ms your interpolation could/would "jump" considerably.[/quote]

It's not exactly the same, but basically the t i use for input to my lerp in the update function never reaches 1.0, so when I go to the physics step there is some movement that still hasn't happened on screen since it only got to 0.8 or 0.75, etc. and then the new physics step will happen and it will jump the position forward and start lerping towards the next one, etc.
[/quote]

I should have considered that, it's one of the reasons I didn't like lerping in the first place because dealing with the fractional times is somewhat of a pain. What I mean by fractional times is that typically you will always enter and perform your physics a bit later than whatever rate you specify. So, if for instance you want to hit your physics every 15ms, it is more likely that you will hit at 16, 17 or 18 and never actually hit at exactly 15. So, you have to take into account for this time delta or you end up not properly using the full proper range of the lerp.
Ok so I changed the implementation to be like this:

Vector3 tpos; // the position we want to get to
Vector3 tvelocity; // the direction from the current position to the one we're going to

// Init code, just run once when code gets activated first
void Start () {
tpos = transform.position;
tvelocity = Vector3.zero;
}

// Physics step
void FixedUpdate() {
tpos += KeyStateToVector(GetKeyState());
tvelocity = tpos - transform.position;
}

// Every frame
void Update () {
transform.position = transform.position + (tvelocity * Time.deltaTime);
}




But it's still sort of a "springy" feeling, and when i go forward and then press say left i get a round nice change in direction, and while it looks good and all I need it to twitch reaction that changes direction instantly. Not trying to be ungreatfull but I just can't get to work the way I want.

Did you look at the update-based solution that I wrote a couple of posts up? Is that a solution that could work for a real game?

But it's still sort of a "springy" feeling



What I believe most action games do (from working with them and reading about techniques over many years) is something very similar to "displayState = oldState + (newState - oldState) * (timeSinceTick / tickSize)"

This means that the physical simulation will be displayed up to one simulation frame behind real time. If you additionally have a lot of graphics command stream buffering, you'll build up a lot of latency. Thus, I recommend using only double-buffering for graphics frame buffers, and doing lock/copy tricks in the command stream to prevent the graphics card from buffering too much command data.

Assuming you have a good timer in your game loop (QueryPerformanceCounter() on Windows, for example), your loop looks something like:


lastTime = now() - simTickSize;
forever() {
curTime = now();
if (curTime >= lastTime + simTickSize) {
int n = 0;
while (curTime >= lastTime + simTickSize && n < 5) {
oldState = curState;
simulate_one_step();
lastTime += simTickSize;
n += 1;
}
lastTime = curTime - fmod((curTime - lastTime), simTickSize); // snap to current step
}
render_state(oldState, curState, (curTime - lastTime) / simTickSize); // lerp between 0 and 1
}



This loop will drop time if you fall more than 5 simulation steps behind, which will probably cause server-side corrections at that point -- choose your max N carefully. If the client machine is simply too slow to keep up the simulation, there's not much you can do, and you don't want to lock the CPU for too long just running simulation without display.

The input latency here will be at least two render frames, because of double-buffering plus the draw-behind. However, that's only 30 milliseconds, which actually will feel very snappy. Most modern online games actually have more in the 5-7 frames range of latency from input to display on screen. (Where "frames" means 60 Hz sim/display steps)

Another option is to use vsync, set the display frame rate to 60 Hz, and set the simulation rate to exactly 1/60 second. Consoles can do this a lot, and it would allow you to drop one frame of perceived latency. With Unity, that may be harder, though.
enum Bool { True, False, FileNotFound };

Ok so I changed the implementation to be like this:

Vector3 tpos; // the position we want to get to
Vector3 tvelocity; // the direction from the current position to the one we're going to

// Init code, just run once when code gets activated first
void Start () {
tpos = transform.position;
tvelocity = Vector3.zero;
}

// Physics step
void FixedUpdate() {
tpos += KeyStateToVector(GetKeyState());
tvelocity = tpos - transform.position;
}

// Every frame
void Update () {
transform.position = transform.position + (tvelocity * Time.deltaTime);
}




But it's still sort of a "springy" feeling, and when i go forward and then press say left i get a round nice change in direction, and while it looks good and all I need it to twitch reaction that changes direction instantly. Not trying to be ungreatfull but I just can't get to work the way I want.

Did you look at the update-based solution that I wrote a couple of posts up? Is that a solution that could work for a real game?


It's a bit strange since for all intents and purposes this should effectively be identical to the lerp case until you get into latency hiding and such. It's been a while since I wrote any code related to this but basically:

p' = o + (n-o) * dt
is the same as:
f( p' = p + v * dt )
over the same delta times, since v is just set to (n-o) outside of the loop.

The only reason I prefer the velocity version is because everyone is used to it and you can fiddle with v to hide network problems at a later point.

As to the other solution you mention, sure you can make it work. I'd have to think about it and probably put together a test for it to see how well it works when integrated into correction, latency etc code.

This topic is closed to new replies.

Advertisement