🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Interpolation on Client Between Server States

Started by
12 comments, last by lifesuxtr 4 years, 7 months ago

I have an authoritative server which is sending position data to client every 100 ms. Ping between client and server is about 70 ms.(which is actually a variable)

I have few questions regarding to interpolation between 2 server states recieved by client.

1-) Where should i place my interpolation code ? Inside my clients update method or should i interpolate whenever i recieve 2 update from server ? 

Example:


 fun onWorldData(data:JSONObject){ // called when update arrives from server.

//initially serverUpdate1 and serverUpdate2 are empty objects.
  serverUpdate1 = serverUpdate2
  serverUpdate2 = data // data is new data which is just arrived from server


if(serverUpdate1.has("x")){ // when at least 2 updates arrived interpolate and set position(just for 1st call)
   

    var blackPieceBodyX = Interpolation.linear.apply(serverUpdate1.get("x").toString().toFloat(),serverUpdate2.get("x").toString().toFloat(),0.15f) // how to calculate 3rd parameter ?
    var blackPieceBodyY = Interpolation.linear.apply(serverUpdate1.get("y").toString().toFloat(),serverUpdate2.get("y").toString().toFloat(),0.15f)

    blackPiece.body.setTransform(Vector2(blackPieceBodyX,blackPieceBodyY),0f) // this code sets position of body

}







    }

As you see interpolation code is called when i recieve update from server, not all the time inside update.

2-)How do i calculate 3rd parameter of Interpolation function ? In above code i was just experimenting with arbitrary numbers.Since server sends it every 100ms and ping is around 70,what should be 3rd parameter of interpolate function and how to calculate it ? Since for sure ping will be part of that calculation formula should i ping server to check ping to use in that formula all the time ? Or timestamp each serverupdate sent by server and when recieved get clients time and substract them ? 

3-)Another approach i tried is this:


    fun onWorldData(data:JSONObject){ // called when sv update arrives to client.

serverUpdate = data
timeSinceLastUpdate = 0f



    }

fun update(){

if(serverUpdate.has("x")){ // if server update arrived
var blackPieceBodyX = Interpolation.linear.apply(blackPiece.body.position.x,serverUpdate.get("x").toString().toFloat(),timeSinceLastUpdate/updateTime)  
  var blackPieceBodyY = Interpolation.linear.apply(blackPiece.body.position.y,serverUpdate.get("y").toString().toFloat(),timeSinceLastUpdate/updateTime) 
  
  blackPiece.body.setTransform(Vector2(blackPieceBodyX,blackPieceBodyY),0f) //set position of body to interpolated values.

timeSinceLastUpdate += deltaTime

}
}

As you see in this example i interpolate between interpolated position of body and serverUpdate, not exactly interpolating between 2 server states inside my update method.updateTime is ping+serverUpdateRate since ping is around 70 and server sends updates every 100 ms it should be like 170(?) or this approach is completely wrong i am not sure.

Right now i am not sending timeStamp with serverUpdate from server but if it is necessary for interpolation 3rd parameter calculation i can do that.

 

Thanks

 

 

 

 

 

Advertisement

It all depends on what you are trying to do.

One possible purpose is to smoothly shift items from their last known position to a "true" position from the server. In that case you set the position it is at now, the position you want to go, and interpolate every frame to smoothly update it.

For more complex code, network clients use timestamps because they live at a different time than the server. In that case the client can do corrections based on the times.  For example, the network packet says the timestamp was accurate 70ms in the past. So it looks up where it thought the client was 70ms in the past, and sees that locally it thought it was somewhere else.  So the client computes the difference from where it thought it was 70ms ago and where the server says it was 70ms ago, then looks at where it thinks it is now relative to where it thought it was 70ms ago.  Now it knows (A) how far apart it was from the server 70ms ago, and (B) how far apart it is from itself 70ms ago. The code can then apply A to B to compute where the corrected location should be right now.  SInce you don't want to pop. you can apply that transion of (A+B) across several frames with an interpolation. 

The second one is more work, but can provide a better game experience, and it is also built in to several engines and networking libraries.

You might be trying to do something else, which is also fine.  Interpolation is just looking at to values --- where it used to be and where it is going --- and then over time shifting it between the two.

I guess i already know what u wrote in your answer but this is not enough for me to implement it with code. Internet is full of those kind of answers i need more spesific one.I am just applying force to a body and returning positions to client and trying to smooth movement.thats all.

What abilities do you have in the physics API?

Do you have the ability to read the position and orientation and velocity and spin of a body?

Do you have the ability to write the position, orientation, velocity, and spin of a body, without the physics engine blowing up?

If so, you can implement a "checkpoint object state" function and a "restore object state function."

Now, call this functions once for each time you happen to be simulating, and store a cyclic buffer or queue of the last, say, second of states. (Because you use variable time step, this will be of variable size.)

Now, when you receive an update "in the past" from the server, calculate what your physics engine thought about the state at that time, and compare to the update. If the update is "different enough," then update the state of the physics body, and re-run the simulation based on the checkpoint.

What "re-run the simulation based on the checkpoint" means depends on your physics engine. For example, if you need object-to-object collisions, you need to restore the state of ALL objects that are possible to affect from the updated body, and re-run the simulation. (This might transitively mean restoring the state of ALL simulated objects.) Which time step size you use here is also up for grabs, because at this point, it's not "the length between updates;" it's some arbitrary value you choose to wind forward to the current point in time.

If you don't have the ability to reset object states, and/or don't have the ability to re-simulate object state, then you can't update objects in the past, and thus can't use physics-based extrapolation. Instead, the least bad you can do is probably to calculate the difference between what the state was at the time in your engine, and what the state was from the server, and then apply that same difference to the object NOW. So, if the server said the object was 1 meter to the right, was moving 2 meters per second faster, and was spinning 0.3 radians per second faster around Z BACK THEN, then you would apply a position update of 1 meter to the right, velocity update of 2 meters per second faster, and spin (torque) update of 0.3 radians per second.

This may of course cause the object to collide with walls or whatever. Them's the breaks, when you have a physics engine you don't control. Later updates from the server will fix this.

Also, and this is important: You will keep getting updates from the server, so you have to keep a running total of how much delta you have applied. You then have to subtract this running delta from the delta provided by the server. Otherwise, you'd apply the full "1 meter to the right" for each update you got from the server.

enum Bool { True, False, FileNotFound };
void afterSimulationStep() {
  now = currentSimulationTime();
  pruneSnapshotHistory(now - to_seconds(1));
  state = snapshotState(playerEntity);
  addToSnapshotHistory(state, now);
}

void onOldStateReceivedFromServer(oldState, oldTime) {
  now = currentSimulationTime();
  stateA = getStateBeforeOrAtTime(oldTime);
  stateB = getStateAfterTime(oldTime);
  midState = stateA + (stateB - stateA) * (oldTime - stateA.time) / (stateB.time - stateA.time);
  deltaState = oldState - midState;
  if (deltaState.isDifferentEnoughToCorrect()) {
    clearHistoryAfterTime(oldTime);
    while (true) {
      addToSnapshotHistory(midState, oldTime);
      oldTime += 0.0167;
      if (oldTime >= now) {
        break;
      }
      midState = simulateStateByTime(midState, 0.0167);
    }
  }
}

enum Bool { True, False, FileNotFound };

@hplus0603 i am using box2d so i guess i have all abilities. What if i just apply force on server and just return positions to client and set them without running physics simulation on client, is it not possible to achieve a smooth movement using interpolation between 2 server states ? Or do i have to apply force also on client to achieve smooth movement ? (by the way mine is just applying 1 input, you can think like kicking a ball each turn. so its not a fast paced game. you can think like turn based ball kicking simulation :) )

what is

  pruneSnapshotHistory(now - to_seconds(1));

function is doing ? especially to_seconds(1) ?

what is

      midState = simulateStateByTime(midState, 0.0167);

and what does 0.0167 mean ? what is simulateStateByTime function doing ?

My first idea was since i have 2 server states, i can interpolate between them until i recieve next state from server like this. So that between server updates client will observe smooth movement ?

if(bothServerUpdatesArrived){    
while(timeSinceLastUpdate <= serverUpdateRate){   
     var blackPieceBodyX = Interpolation.linear.apply(serverUpdate1.get("x").toString().toFloat(),serverUpdate2.get("x").toString().toFloat(),timeSinceLastUpdate/serverUpdateRate)     
   var blackPieceBodyY = Interpolation.linear.apply(serverUpdate1.get("y").toString().toFloat(),serverUpdate2.get("y").toString().toFloat(),timeSinceLastUpdate/serverUpdateRate)        blackPiece.body.setTransform(Vector2(blackPieceBodyX,blackPieceBodyY),0f)        
Thread.sleep(1)        
timeSinceLastUpdate +=1   
 }}

But i guess this is completely wrong ?















What if i just apply force on server and just return positions to client and set them without running physics simulation on client, is it not possible to achieve a smooth movement using interpolation between 2 server states ?

Remember that the client always seems to live in the past due to lag.

If the server is doing ALL aspects of the physics simulation, and the client is running as a view-only display of the physics, this can be visually pleasing although it suffers from lag. The client receives all the position information and physics information, accepts it as authoritative, and runs it until it gets the next update and repeats.

In that scenario the client can run its own physics updates to provide a visually smooth result, but because game physics are almost never deterministic it will quickly drift out of sync with the server. Exactly how smooth it looks depends on latency and bandwidth.

If the client needs to interact with the physics then the client will always be lagged behind the server.

what is pruneSnapshotHistory(now - to_seconds(1)); function is doing ?

System like the one described operate with a sliding window. They track all the changes within a time window, getting bigger and bigger as time passes. When the other side reports that they have the data, old values in the time window can be discarded. In that code, the time window is reduced to one second by pruning old values older than now minus one second.

As an example of that with updates, let's say there is a snapshot A, so it tracks {A} in the history. Now there is another update B, so the history contains {A, B}. Now there is another update C, then another D, so the history contains {A, B, C, D}. When state is received from the server, there is a call to clearHistoryAfterTime(). Let's say that time is the same as B, so item B and everything before it is removed, leaving the window with items {C, D}. Then snap shot E is added, giving {C, D, E}. Over time the number of items in the window slowly slides. Maybe {C, D, E, F}, then { C, D, E, F, G}, then {F, G}, then {F, G, H}, then {F, G, H, I}, and so on.

This is useful for many things. In networking it allows the system to retransmit old data if there was a network glitch. To help with lag, it allows you to rewind to a previous state so you can redo the simulation and replay it with missing data. It can be used to help identify cheaters, and for many other purposes.

and what does 0.0167 mean ?

That's about one graphics frame at 60 Hz. 1 second / 60 is about 0.0166...67 milliseconds.

what is simulateStateByTime(midState, 0.0167); function doing ?

It is simulating one graphics frame.

That will advance the simulation by a single step. The loop runs by advancing one graphics frame at a time until it has caught up by testing (oldTime >= now).

The loop runs by advancing one graphics frame

Well... one simulation frame. Whether those are really tied to physics or not depends. When you run display and physics in the same time, then yes. But in this case, you don't draw the item after each simulation step; you're just interested in the outcome from the simulation. But you have to pick SOME amount of time to advance by, incrementally, until you get to the "current time," because trying to take a single simulation step that's, say, 0.8 seconds long, is unlikely to work well.

That being said: If you want to apply forces on the server only, then yes, you can build a robust system, that doesn't need extrapolation, or client-side prediction! The main draw-back is that control input will be "laggy" -- when I press a key on the client, it will have to make it to the server, and then the server will have to apply it, and then send the result back to me, before I draw it.

It's totally OK to display an interpolation between the last two received server states. You don't need physical simulation for that; just interpolate position and orientation. There may be some amount of "jumping" when you receive the next update, if there is transmission jitter. You can work around this either by buffering more, or by interpolating between extrapolated states. (The latter is what the EPIC interpolation library does.)


enum Bool { True, False, FileNotFound };

Thanks for very informative answers undefinedundefined and undefinedundefined

But for now i want to implement just an interpolation on client and check how it will look/perform.Since i dont really care about input lag, if i just achieve smooth movement on client with interpolation, it will be enough for me.

So, i still have questios about how to implement good interpolation on client between server states. Server sends update every 100ms.If performance will be not good with 100ms,i can make it 60 ms too.

So one approach is this. Lets say i just have one body and sending / updating its positions.

fun onServerUpdate(data){
serverUpdate = data
timeSinceLastUpdate = 0f
}
fun update(){
if(serverUpdateArrived){
var shooterBodyX=Interpolation.linear.apply(shooter.body.position.x,serverUpdate.get("x").toString().toFloat(),timeSinceLastUpdate/100)
var shooterBodyY=Interpolation.linear.apply(shooter.body.position.y,serverUpdate.get("y").toString().toFloat(),timeSinceLastUpdate/100)

shooter.body.setTransform(Vector2(shooterBodyX,shooterBodyY),0f) // set position of body


}
timeSinceLastUpdate += dt*1000 //(since deltatime is in seconds convert to miliseconds ? )
}

I guess this approach is wrong because if i write a constant like 0.2 to 3rd parameter of Interpolation.linear.apply(), i observe better but still bad results.

Another approach is saving 2 states to 2 variables as i posted on my question,and interpolate between them.

fun onServerUpdate(data){
serverUpdate1=serverUpdate2
serverUpdate2=data
timeSinceLastUpdate = 0f

}
fun update(){
if(bothServerUpdatesArrived){
var shooterBodyX=Interpolation.linear.apply(serverUpdate1.get("x").toString().toFloat(),serverUpdate2.get("x").toString().toFloat(),timeSinceLastUpdate/100)
var shooterBodyY=Interpolation.linear.apply(serverUpdate1.get("y").toString().toFloat(),serverUpdate2.get("y").toString().toFloat(),timeSinceLastUpdate/100)

shooter.body.setTransform(Vector2(shooterBodyX,shooterBodyY),0f) // set position of body

}
timeSinceLastUpdate += dt*1000
}

I guess this approach is also wrong

So how do i implement robust only interpolation system ? how to calculate 3rd parameter of Interpolation.linear.apply() ?

t


Your second approach is closer to the way it needs to be. Interpolation is just "find me a value somewhere between A and B". So it only works when you have 2 positions to interpolation between. You guess that it "is also wrong" but you've not said why you think it's wrong or what you're observing.

The main bug I can see is not the interpolation but your timekeeping - you're attempting to interpolate with a fixed time value (dt*1000) but that doesn't reflect the passage of time, so the interpolated values won't change either. You need to care about how long has elapsed since you last received something from the server, which implies you need to keep adding to timeSinceLastUpdate each time you update.

Other recommendations:

  • For performance and clarity, I strongly suggest you do all that toString/float stuff just once, when you receive the data. Extract the values, store the relevant Vector2. Then, your update system only ever has to deal with the numbers/vectors.)
  • I suggest you use a floating point value for time, and measure it in seconds. Trying to measure in milliseconds with integers is not worth the trouble.
  • Once you get the basic interpolation working, consider the fact that you won't always receive messages exactly 100ms apart. A simple way to start compensating for this is for the server to include a timestamp in the updates, and instead of applying the received one immediately on the client, you can buffer them up and use them slightly later. This adds a bit more latency but smooths the movement even further and avoids extrapolation errors if a message arrives a bit late.


This topic is closed to new replies.

Advertisement