Howdy!
Welcome back once again to my weekly blog series exploring latency mitigation in networked games! Last week we made a simple player controller and networked that controller in a naive way so that it would send its inputs to a dedicated server that would validate the inputs and update the player's position accordingly:
This results in choppy gameplay locally for the client which is what we are correcting this week!
Refresher on Client-Side Prediction and Server Reconciliation
As we talked about last week, the client needs to have its input validated by the server so that no client can create an unfair advantage for themselves by spoofing their position or other movement data. However, as shown above, waiting for the server to acknowledge all inputs can lead to really choppy gameplay which is not ideal for any real-time multiplayer game.
This is where the idea of Client-Side prediction comes in. Building off of the assumption that most players are not malicious and will not be faking their movement, we will let the player apply their input locally immediately and then correct any mistakes in the position calculations later once the server acknowledges the movement:
And that's exactly what I implemented this week!
My Implementation
The first step in implementing this was to apply the player movement locally as soon as the input is read:
ApplyMovement() updates the player position in the exact same way as we were last week. This application of input immediately is the client-side prediction piece of this implementation. Client movement will now be reflected immediately, but we still have a bit more to do to make sure the client and server don't get out of sync Importantly, in addition to updating the player's position automatically, we also push the most recent input struct into a Queue of FPlayerMoves called savedMoves which will come in handy later when we need to reconcile the client's position. After input has been read, it is sent to the server with the updated ServerMove function which now looks like this:
The server applies the movement just the same as the client and then builds up a FServerAck, which consists of the moveID (or input timestamp) and the resultant location of applying the input to the player. This FServerAck is then sent back to the client with the newly create Client RPC ReconcileMove()
This function reconciles the client's predicted movement with the updated position received from the server. It does this by looking at the input timestamp and popping all Player Moves off of the savedMoves queue (which I had mentioned earlier) that are less than the current input timestamp received from the server (which covers any lost packets). Then, we set the client's position to the position that was just acknowledged the server and iterate through each saved move in the queue that the server has yet to acknowledge reapplying the input to the player. The result of this is that all player moves are still performed and validated on the server, but the client sees smooth gameplay no matter how bad the network traffic is. And that's client-side prediction! Below are two videos showing this implementation tested under Average network traffic and Bad network traffic:
Average:
Bad:
As you can see, there is hardly a difference between the two videos despite there being more than twice as much latency on average in the second video. You can see the delay between the server and client visualized if you look at the difference between the Number of Acked Moves and the Number of Sent Moves at any given moment in the videos above.
Comparing these videos with the videos from last week which used the same network settings it is clear that we have made a significant improvement to our latency mitigation. Next week we will be starting to work on mitigating the latency of other networked players because, spoiler-alert, client-side prediction will unfortunately not help us in those situations.
You find my project repository on GitHub here:
Comments