Creating a 2D Multiplayer Racing Game

Introduction

How do I create a 2d multiplayer racing game with lag and packet loss compensation and desync-avoidance? To find this out I will take a look at the Photon Unity3D Networking Framework and create a project using Photon for the Unity Engine, to research this very topic.

During this project I will research various industry examples and/or documentation to find my own optimal solution to the problems I will face making this application.

I will try to implement my own synchronization code based on the research done and document the process and the things I found out. In the last chapter I will explain my own solution and try to argue why I think it is the optimal solution for my use case.

Setting up the project

Before I can start my research I will have to set up my project with the Photon Unity3D Networking Framework. Since this is not the main focus of my report I will try to spend as little time as possible on this. To actually get to a context where players, steering their cars, will have to synchronize the cars on their client with the other’s, I will first have to do some setting up.

Connecting to a Photon Server

The first step is that you connect with Photon’s servers (Also called the master server). There are 4 ways to connect to a Photon server. 2 of which are cloud servers. Here are all 4 in code:

Figure 1. Connecting to Master options

The first option (and the one I’m using) uses a scriptable object called PhotonServerSettings to connect to a Photon Server based on settings configured in that scriptable object. The second option gives you the option to fill in a specific master server address, a port and your app ID for verification. The app Id is related to the Photon Project from which you are making the call. The 3th option connects you to the best Photon cloud server based on ping and the fourth gives you an option to connect to a Photon cloud server in a given region.
Facilitating connecting to Master in a unity scene, I did with a simple “Connect to Master” Button

Figure 2. The Connect to master button

Connecting to a Room

The second step is that you connect to a room. In a room is where you start connecting with players, sending data back and forth. There are 4 ways to connect to a room. Here are all 4 in code:

Figure 3. Connecting to Room options

The first option (and the one I’m using) tries joining a room with a given name, room options and lobby type and creates one if there isn’t one already. The second option tries joining a random available room. The third option joins a room with given name and the 4th and last option creates a room with given name and given room options. Joining a room will fail if no rooms have been made yet. This is why I use the first option so I will always have a guaranteed room even if none has been made already.

Facilitating connecting to a Room, I did by updating the “Connect to Master” button to a “Connect to Room” button when connecting to master was successful.

Figure 4. The Connect to room button

The Room process

When inside the room, there needs to be a way to start the game. There needs to be a definition as to when the game can be started and there also needs to be some sort of authoritative client to actually make the call to start the game.

Looking at how a lot of games handle this room setup scenario I think I will follow the lead by introducing ready buttons for each player to communicate their ready status to others. When the last player to ready up has done this, there needs to be the authoritative client to check if all players have readied up or not.

Instead of having a server-client model, Photon provides a master client option, for developers to give additional rights to or give authoritative power. In this room setup scenario, I decided that It would be the master client to check on the ready status of all players and to decide on whether to start the game or not.

Facilitating this I added, for each player, a player info item with a ready button. Players can also see, in the top left corner, information on the room they are in.

Figure 5. One player inside the room

Figure 6. Two players inside the room

Wanting to visualize the process from opening the application to actually starting the game, I created some diagrams.

Researching Industry Examples

The first big company that I found had some interesting information on Multiplayer Networking was Valve. They had 2 interesting pages which included a lot of documentation on their “Source” engine and its networking, going very much in depth about tick rates and lag compensation. I found a GDC talk by Glenn Fiedler, also known as Gaffer On Games which included a lot of basic knowledge on the different categories of synchronizing objects and optimizing networking especially related to Physics.

Valve their Source engine

On their documentation page about their software they give a short definition of what a server is. “Usually a server is a dedicated host that runs the game and is authoritative about world simulation, game rules and player input processing” (Bernier, 2019). Communication is done by sending small data packets at a high frequency. The clients receive the current world state and generate video and audio output based on these updates. The server receives sampled inputs from the clients so it can process these for a new world state update.

Important to note here is that this description relates to a Client-Server networking architecture. In this architecture, clients only communicate with the server and not with each other. A peer-to-peer application would facilitate this (Bernier, 2019).

Making this work is hard because of the following reasons:

  • Network bandwidth is limited, so the server can’t send a new update packet to all clients for every single world change.
  • Network packets take a certain amount of time to travel between the client and the server. This means that the client is always a little bit behind the server time.
  • Client input packets are, because of reason 2, delayed on their way back. This means the server is always processing temporally delayed user commands.
  • Each client has a different network delay which varies over time due to other background traffic and client’s framerate.
  • Issues like lag (where packets take longer than normal to travel), packet-loss (where some packets don’t reach the destination) and jitter (where time between packets varies a lot) can cause strange effects to normally smooth gameplay.

Figure 7. Game server ticks visualized

Making this work is hard because of the following reasons:

  • Data compression (reducing data footprint when sending, reconstructing it when delivered)
  • Lag compensation

The client can then use predictive measures (for example extrapolating) and interpolation to improve the experience for the player.

In his chapter about Entity Interpolation Bernier goes in depth as to how the interpolation is done exactly. Since snapping entity states to a given world state would look choppy and jittery (visualized when implementing my own code), clients go back in time for rendering, so positions and animations can be continuously interpolated between two recently received snapshots.

In a scenario where you receive 20 snapshots per second, a new update arrives about every 50 milliseconds ((1 second / 20) x 1000). If the client render time is shifted back 50 milliseconds, entities can always be interpolated between the last received snapshot and the snapshot before that.

In the “Source” engine, the client buffers snapshots based on a constant interpolation period value. This constant value defaults to 100 milliseconds. This means that even if a snapshot is lost. The client can still linearly interpolate between the snapshot before the one it lost (in the buffer) and the one received after the one it lost.

Figure 8. Interpolation between old snapshots visualized

Gaffer On Games GDC Talk

Glenn Fiedler, the founder and CEO of Network Next, Talked during his GDC talk about Networking for Physics programmers. My 2D multiplayer Racing Game uses a lot of physics with car interaction and control, the things he talks about are very relevant to my project. The talk contains a lot of information of which not all is used for my own networking code. Despite that, the information that was relevant really helped bring my project further. The things I was most interested in in his talk was his explanation of the Three Techniques for network synchronization that he distinguishes: Deterministic lockstep, Snapshot interpolation and State Synchronization.

Deterministic Lockstep

Deterministic lockstep is used a lot in real time strategy games where inputs generated on one computer are always the same as on another computer (Fiedler, 2018). This means that the computer’s hardware doesn’t determine the outcome of the input. There are no dependencies there. A low budget computer gives the same result as a high budget one. When a developer chooses to use deterministic lockstep, he only sends and receives player input (for example keyboard input). These inputs can then be used on the client to simulate the behaviors associated with these inputs. This method doesn’t use a lot of bandwidth but requires the game to be deterministic to the byte level.

Snapshot Interpolation

Developers using snapshot interpolation send, in their snapshots, the necessary states that describe an entity in the world. This would mean for a car its position and orientation, and maybe even its velocity. Other clients receiving these states can then interpolate between the states in their scene and the states received in the snapshot. The other clients are interpolating between snapshots. This method is very robust but costs a lot of bandwidth (Fiedler, 2018).

State Synchronization

State synchronization is a method where you as a developer make use of both input and states that describe an entity. With this, you don’t require determinism because you have, alongside the inputs for the simulation, a state to which interpolation can be done to fix problems up (Fiedler, 2018).

Implementing my own synchronization code

Having looked extensively at research on the topic of multiplayer networking I can now create and test my own code in the Unity Game Engine using the Photon Unity3D Networking Framework. Before I can start writing my own code I have to set up my scene and cars to facilitate this.

I created a road on which players can drive their cars and imported some simple car sprites to represent each player’s car.

Figure 9. Road with simple cars to drive on

I will start off by indicating the remote state of the car (i.e. the last snapshot state I received) as a red marker (I can later change this to a more transparent version of the car).

Figure 10. Red indicator to visualize the remote state of the car

I need to test out a lot of different things to eventually get to something I can be proud of and know to be working well. I have to take into account many things when writing my code. My cars make use of a rigid body component to move around and interact with their surroundings. This rigid body component places the car under the control of the physics engine. Since the physics engine produces non deterministic results all the time (Fiedler, 2018), I will have to take this into account when writing my code.

Snapping to newly received snapshots

Knowing why just snapping to a newly received snapshot is not the way to go is always handy.

Figure 11. Code snippet where RB is rigid body component instance

Figure 12.1. Without Lag

Figure 12.2. With Lag

Deterministic lockstep

Sending only player inputs in the context of my game would be to send vertical and horizontal axis inputs (for controlling the car). Doing this, the car movement actually looked pretty good. However, the remote car (shadow car) would come into view a lot, meaning it got out of sync. This was especially true when I started increasing the amount of lag. Also, everything works well until collisions happen and the physics system with its non-determinism creates difference on both clients.

Figure 13.1. Sending only vertical input

Figure 13.2. Sending horizontal and vertical input

Snapshot interpolation

Just interpolating towards the new state of the remote car could also be an option. If I don't simulate anything on my computer and just look at the state that is given to me each snapshot, there won’t be any errors related to non-determinism of the physics engine.

Figure 14. The position of the remote car gets linearly interpolated towards a remote position.

Figure 15.1. Without Lag

Figure 15.2. With Lag

Looking at the movement of the car, it is smooth. Sadly enough it is very much at too great a distance from the remote state it should be in. This could definitely be improved upon. Using a method from a dead reckoning algorithm where u extrapolate the remote position using the lag and velicity (Aronson, 1997).

Testing this out i started with a simple implementation where i retrieve the position, rotation and velocity of the car and update the remote position with velocity times lag

Figure 16.1. Receiving position, rotation and velocity and storing them

And then move towards these “predicted” positions and retrieved rotations.

Figure 16.2. Linearly interpolating to remote position and remote rotation

Looking at the results with high amounts of lag, I found that the linear interpolation actually did a great job. The only thing missing was to create a snap when the position was too far behind or the angle between rotation and remote rotation was too great.

Figure 17. Code snippet where, if a max distance or angle is reached, a snap occurs setting the state directly

Testing this with higher lag amounts, i find that the car does move smoothly but it starts teleporting more frequently, since the speed is lower and the next position/rotation is further away. Increasing the thresholds based on lag did help a lot at higher package loss rates and when having a lot of jitter.

Figure 18. Code snippet where max distance and max angle are updated based on lag

I decided to not expand further on this technique. This is because, while in the process of testing this, I learned more about state synchronization and definitely wanted to try that technique first before expanding too much on one that might not even fit my use case.

State synchronization

After watching the GDC talk on Networking for physics programmers by Glenn Fiedler, i think that this technique fits my game the most. Starting off, I want to test the context in which I used deterministic lockstep and add linear interpolation to change it to a state synchronization technique, sending only position and rotation in addition to input.

Figure 19. Simulating vertical input with sending of position

The result is pretty good in comparison with only the inputs. After the car stands still and lag, jitter and packet loss are increased, the car still manages to stay almost on top of the shadow keeping a smooth movement along the way.

Using the same technique but with rotation linearly interpolated as well:

Figure 20.1. Without Lag

Figure 20.2. With Lag

With a normal connection the remote car (shadow car) is nowhere to be seen which means we are almost on the remote position each time. Even when, after the connection is simulated to be really bad or a collision happens, the car is still smoothly moving around at the same pace it was with a good connection. The only problem comes when the car gets out of sync when the amount of network interference is high.

Finding an optimal solution

Now that my first iteration of my car-synchronization code is finished I can add more things to test its flexibility. I want to create a boost for the car, adding gameplay and a way of increasing the car’s speed. I also want to add more objects with which the cars can collide.

Car Boosting

After working for a few hours on car boosts I noticed that the speed became a problem for synchronization. At higher speeds, the car would be too far behind the remote car causing desync problems. Currently I don't send velocity in my state synchronization, since cars move mostly at the same time, all the time. Now that I have boosts, the car 's speed can fluctuate very quickly and frequently. I can manage this by adding the same technique(dead reckoning algorithm) as I did with snapshot interpolation where I added the velocity times the lagg, to the remote position (see Figure 18).

Figure 21.1. Without Lag

Figure 22.2. With Lag

Now that i have incorporated velocity into my remote position of the car, i have a better approximation of where it actually is, this means the shadow will appear more frequently since i won’t be at the exact position each time because of the delay. I now have to adapt my linear interpolation in a way that it adapts its speed based on some network related value.

Collision problems

Another problem is that ,with that speed, the errors after collisions need to be corrected very fast. Otherwise, even with a good connection, the remote car and the car on our client will be off by too much and I would need to teleport it which i don't want. To mitigate this I want to send a colliding flag. This colliding flag tells the other client if it is colliding or not. If the remote car is colliding, I can increase my linear interpolation speed to try and compensate for the indifferences that may occur.

Thinking about these 2 things i came up with this:

Figure 23. trying to incorporate lag and a remote collision flag into the linear interpolation

Figure 24.1. Without Lag

Figure 24.2. With Lag

The result was already a lot better. Without real network interference, the car would mostly stay on top of the shadow car, even after collision. The problem however, is that the amount of desync created by simulated network interference is way too high. I also still had the problem of collisions pretty much destroying synchronization of the car. Since the remote car will never be at the exact location of impact at the same time as the true car, the simulation itself will be a lot different, this error margin will get even bigger when the speed of the car is increased. To Fix this I have to make sure that the car, even with high speed, would be as close to the remote car as possible.

After testing my new setup with another student I got feedback on my new synchronization. One of the things I had not tried yet was linear interpolation based on distance between position and remote position. One of the things I also had to accept was that my send rate would need to be a lot higher to make the simulation more accurate. According to Photon Network documentation, Send Rate is defined as the amount of packages sent per second. In addition to Send Rate there is another value called Serialization Rate, which is defined as the amount of time the OnSerialize Function is called(this is used to send and receive data).

Creating the Final solution

I realized the linear interpolation I had always had in my Fixed Update could be moved towards the Update so the linear interpolation would happen more frequently.

Figure 24. Moving correction of car simulation to game frames

In the linear interpolation I base my interpolation speed of movement on the distance between position and remote position and for rotation based on the angle between rotation and remote rotation. Render time is replaced by tick time because I realized i am linearly interpolating based on server ticks (time between serialization) and not render ticks (Time.deltaTime/ time between render frames).

Figure 25. Update correction of car simulation

After receiving a new snapshot and updating all my data, I also check if my car is catching up in some way. If the car is catching up and the remote collision flag is true, it is essential to snap the car to its new position to make sure no further deviation from the true simulation occurs.

Figure 26. Checking for snap when receiving a new snapshot

I increased the send rate (amount of packages sent per second) and SerializationRate (OnSerialize Ticks per second) to be higher (SendRate was 20 and SerializationRate was 10).

Figure 27.1. Send and serialization rate constants

Figure 27.2. Setting send and serialization rate

The result:

Figure 28.1. Without Lag

Figure 28.2. With average Lag

Figure 28.3. With high amounts of Lag

References

  • Bernier, Y. W. (2019, April 20). Source Multiplayer Networking. Retrieved from Valve Developer Community: https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
  • Fiedler, G. (2018, oktober 2). Networking for Physics Programmers.
  • Aronson, J. (1997, September 19). Dead Reckoning: Latency Hiding for Networked Games. Retrieved from Gamasutra: https://www.gamasutra.com/view/feature/131638/dead_reckoning_latency_hiding_for_.php
  • Rivenes, L. (2016, juni 21). What is network jitter? Retrieved from datapath.io: https://datapath.io/resources/blog/what-is-network-jitter/