In this article series, we will look into implementing entity interpolation using the features provided by the Unity High Level API, or HLAPI. We will continue from where we left off in another tutorial series, and work gradually in six parts:
In Part 1, we will talk about entity interpolation in general, then we will take our single existing script and break it into smaller scripts. This would hopefully prevent our code from turning into an unreadable and unmaintainable mess, allowing us to add in entity interpolation more easily afterwards.
In Part 2, we will change the way input is sent by the client to the server. At the moment, the client can send input information to the server during every frame, which can cause a lot of network traffic. To fix this, we will change our code so that input is batched then sent to the server less frequently.
In Part 3, we will add some safeguards on the server side to prevent a client from cheating by sending too many inputs.
In Part 4, we will change the way we handle the data that we receive from the server. Instead of synchronizing immediately, we put the data received in a buffer, and then introduce a short delay before synchronization. This is in preparation for...
...Part 5, where we will take the buffered state data and interpolate between them, resulting in smooth movement despite the low frequency updates.
Finally, in Part 6, we will tweak our code to better handle an interpolation edge case. We will also look into using interpolation for something other than position data.
Although six parts might seem to be a bit too much, doing it this way should allow us to break up the task into smaller, more manageable pieces, hopefully aiding in your understanding of the concepts involved and helping you see how you can use the techniques demonstrated in your own games.
This article uses scripts from the first two parts of another Unity 5 Networking Tutorial as the base, so it is assumed that you are familiar with the basics of developing with the Unity 5 Networking High-Level API (HLAPI), and implementing client-side prediction and server reconciliation with the HLAPI.
Our implementation is based on a couple of sources:
Just like before, we reference Gabriel Gambetta's excellent Fast-Paced Multiplayer article series, in particular the third part on entity interpolation. As of the time of writing, the Fast-Paced Multiplayer live demo does not include sample code for entity interpolation, so hopefully developers who are looking to implement entity interpolation in their own projects could borrow from the scripts included in the sample project for this article series.
Our particular design (and probably almost all others) is heavily influenced by the entity interpolation section of the Source Multiplayer Networking article.
It is highly recommended that you read through both of these resources to get a better overall understanding of entity interpolation and its importance in implementing networked multiplayer games.
Download The Project
If you have already downloaded the project folder for the other tutorial series, then you should have received the new project folder for this series as an update. Otherwise, you can download the project folder here.
The What And Why Of Entity Interpolation
The HLAPI makes it very easy to manage distributed state in a networked multiplayer Unity game. Given a variable that we want to synchronize across all connected clients whenever its value is changed, all we have to do is mark it with a SyncVar attribute:
[SyncVar] CubeState state;
"Whenever its value gets changed" needs some additional explanation. The frequency at which SyncVars are actually synchronized is governed by the network send interval. At its default value of 1/10th of a second, this means that a SyncVar is only able to synchronize updates 10 times a second at most.
This can be a problem for variables that change all the time, for instance every time FixedUpdate is called. Consider an object that moves evenly from position 0 to position 50 over 1 second via FixedUpdate. On the server, we will see a smooth progression of values 0, 1, 2, and so on until 50. However, the network send interval would cap SyncVar updates to other clients at 10 times a second. This means that, even in the ideal case, we will initially be at position 0, then a delay, then a jump to position 10, then another delay, then position 20, and so on, leading to choppy movement.
While it is possible to set the send interval to 0 to remove the synchronization frequency limit, doing so can cause too much network traffic, leading to undesirable game performance. We also need to remember that network connections are unreliable, and it is possible for network packets to be dropped occasionally, resulting in missing SyncVar data, as if frames were being skipped. To make smooth multiplayer experiences possible despite the intrinsic limitations of networking, we can use entity interpolation.
In entity interpolation, we embrace the fact that SyncVar updates are present for only a few frames (at most only 10 out of 50 frames in a second) and absent for most others (the remaining 40 or more frames). So to create smooth movement for the clients, we try to fill in the gaps for frames with no SyncVar data using interpolation.
Continuing with our example, suppose we need the position of our entity at time 0.22 seconds, but we do not have the needed SyncVar data. However, we have the SyncVar data at time 0.2 seconds (say, at position 10) and time 0.3 seconds (position 15). Given this information, we can perform interpolation (perhaps linearly) and decide to render our entity at that time at position 11. If we interpolate whenever actual data is absent (instead of doing nothing and making it look like the game has a low frame rate), we are able to hide the effect of low frequency updates.
Note the main limitation with using interpolation: given a missing data point in time that we want to interpolate, we will always need at least two actual data points, one before and another after the needed time. This implies that there will always be a time lag for interpolated entities. For our example, we will use a fixed delay of twice the network send interval (1/5th of a second based on the default value), which would hopefully provide better resiliency against dropped packets.
Six parts? Seriously? I do not have the time right now!
The source code for all six phases is already available for download, and you do not need to wait for the next articles to be published. All you have to do is grab the project folder so that you can try out a working entity interpolation implementation for Unity on your own machine right now.
By stepping through the implementation over six phases, hopefully it should be easier for one to understand how all the pieces fall into place, referring back to the articles as needed to fill in the gaps if some parts of the code are not clear enough.
Our Starting Point
- As a local player, we read the input device, then...
- As a client, we receive updates from the server, then...
- perform reconciliation on the local player, and...
- make sure that all player objects show the correct state.
- As a server, we:
- give player objects their starting states, and...
- receive inputs from all the clients, then use SyncVars to propagate the effects.
Our script did a lot of things, which is not ideal as far as being a component is concerned. At this point, someone new to the script would not find it easy to pick it up and extend it.
To solve this, we can take our list above and use it as a guide for breaking our large script into smaller, more manageable chunks:
We can have all the server-related tasks listed in (3) in a single script, then use this component only if we are on the server.
We can have a script that focuses on client side prediction (1a) and server reconciliation (2a), then use this component only if we are a local player.
We can have the code for clients that are not local players in a separate script. This is where the interpolation code will go later on.
Input handling should go in its own script as well. It is tempting to combine input handling with client side prediction since they are both needed only by the local player. Keeping them separate would make it simpler for us to support additional types of input sources (mouse, touch, VR, whatever the future holds) while minimizing the risk of messing up the already-working client side prediction code.
It should be noted that, in our case, client side prediction and interpolation are mutually exclusive, but they both make use of the data received via SyncVar. Thus it makes sense to define a common interface that handles SyncVar data, then create different implementations for prediction and interpolation.
What If We Were Starting From Scratch?
If we were starting from scratch instead of adapting an existing script, this article would still be applicable. The division of tasks listed above should serve as a useful guide in deciding which components we should plan to build and how to divide responsibilities among them.
NetworkBehaviour vs. MonoBehaviour
For our particular design, let us decide to have just one NetworkBehaviour, which will contain our SyncVar (server-to-client communication) and Command (client-to-server communication). For all the other functionality that we broke out of the original script, we can just use plain MonoBehaviours. On each MonoBehaviour, we just need to keep a reference to the single NetworkBehaviour so that we can still send and receive data across the wire whenever needed. This should keep our design simple, make it easier for us to isolate network-related issues, or even allow us swap out UNET with another networking system if we wanted to.
It is around these design considerations that we broke our original script into smaller pieces. You can download the project folder and see how everything turned out. We have not added any new functionality yet, but the work that we have done here should make it easier for us to add interpolation later on.
And that concludes Part 1. Next time, we will look into changing the way we send Commands to the server. Until next time!