Mutiplayer tank game

Introduction

Background

Back in 2015, shortly after I finished my first first ever programming tutorial, I made a small tank combat game. I am proud of this project as it was the first game I made on my own and it was quite complicated despite my lack of coding knowledge at the time. The game had a selection of maps, a team toggle, power-ups, and multiple gamemodes. Over the years I thought about remaking this game with more advanced technology and my ever growing knowledge and ambition. This sememster proved to be the perfect opportunity to start working on this remake.

Description

In this project I’m going to make a fast online party tank game. On this game players join a server and shoot each other to score points. Whoever scores a certain amount of points first wins.

Scope

The scope of the project is to create a working gameplay loop. A player must be able to host a lobby and have other be able to join them. While the game is active the server must synchronized the actions of all players with as few technical problems as possible. Players must be able to shoot each other and score points. Finally the server must be able to keep track of everybody’s score and be able to correctly determine the winner.

Goal

The goal is to learn about the technical knowhow of online networking. I will learn about designing a server architecture, how to synchronize actions between computers, and adjusting for lag and other connection problems. This goal is achieved by making a game that has a working gameplay loop.

Development

Server architecture

Choosing a server architecture

There are several server models used for online multiplayer. A server based architecture has the game run on a remote server to which all players connect to. In this method all player data is first send to the server, the server validates that information, and is then send back to all the players. This method requires more work for the developer, but makes it harder for a player to cheat and all movement is more synchronized as all data is validated first.

Another method is the client based architecture. In this architecture the server is hosted on the computer of one of the players and all calculations are done on their end. This method is easier for the developer but brings it’s own set of problems. The hosting player can easily cheat and they have a inherent advantage as they have more accurate information then the other players who have to wait for all the data to travel the network. This method is mostly used for co-op games.

Finally there is peer to peer. This server architecture has all player in a game connected to each other. This server architecture does result in the most accurate gameplay for all players. However this requires a lot of work for the developer and doesn’t take advantage of technology improvements created since this method implementation since the 90’s. Therefor this server architecture is considered outdated.

The game I want to make is a fast paced player vs player game where everybody should be on even footing. Therefore this game shall use a server based client.

Creating a player

I’ve created a player controller to start testing using a tutorial I followed during the summer break before I started this semester. I used the Unity Netcode package as it’s the only networking package I’m familiar with because of that tutorial.

With this the player can move, rotate, and the barrel will follow the camera

Using built-in packages like Cinemascine and the new Input manager I was able to quickly get a working tank that coud move around. I got the top and barrel to rotate but it looked weird, need to do more testing on how it looks for others.

First time online

Testing

Test goal:
The goal of the test was to see if I could connect 2 or more players using unity Transport and see if it would syncorise the movemnt of the players.
Test results:
I could connect the players and both would spawn in the same scene. However the players didn’t move at same speeds. The host moves faster. The game still runs on a host based architecture at the moment. I’ve also had a issue that the camera of the host would only look at the most recently spawned player. However this is something I was able to fix right away by simply setting the camera priority to zero if the player was not the local client.

Connecting players

Connecting computers

For the second week I wanted to start connecting 2 computers to each other so that I can get more accurate overview how the game behaves over the network.  

A quick way to connect my computer and laptop should have been to simply to give my ip address to the Unity transport component, however this didn’t seem to work. Looking into the Windows command prompt revealed that my desktop and laptop had different ip addresses despite begin connected to the same network. There is a method called port forwarding to to connect computers but this involved messing with a router and is not sustainable for an online game.

Doing research for a more permanent solution I learned about Unity relay package. This package allows players to send messages to each other through Unity’s Relays servers. When a Relay is created it creates a public IP adress that all players can join, meaning that players themself don’t have to share Ip addresses. This sounded promising so I went to work to add it to my project

Or that’s what I should have been doing. I misunderstood what I needed to do to add a Relay system to my project. I thought that I also needed to have a lobby system in my project to access relay. While the two systems work hand in hand, the two aren’t depended on each other causing me to spend a bunch of time on something that wasn’t important until later in the production cycle.

The player signs in with the relay when the game is started
Creating a Relay server for others to join
Joining a Relay

I added the relay system to the project and it worked. I was now able to connect multiple computers to a single server to have an online game. 

Now that I had working online multiplayer I wanted to see how it behaved. I was at my old workplace and asked a bunch of my colleges to download the game and join the server. Immediately it became apparent that there were synchronization issues like the video bellow.

The host (left) rotates much faster than the players, giving them a unfair advantage. I added an extra function that a computer could run as server instead of a host and that mostly fixed the problem. However a more proper solution needs to be implemented to ensure a good gameplay experience for all clients.

Client synchronization

To have a successful online game measurements must be taken to ensure the quality of the connection between players so that everybody is on a even playing field, as not everybody has a optimal setup to play a online game.

Client prediction and synchronization

The typical client-server setup doesn’t save the position of the players. What happens instead is that the client sends a payload of information including data like the timestamp of when it is send and the player input. The server uses this payload of information to calculate the movements of the player and sends back a payload to move the players on the clients end.

The local client however doesn’t need to wait for the server. It already receives the input from the player so it can already start moving. This however can be problem if the player client or server has trouble communicating to each other and can lead to a desynchronization.

In this example the player model has desynchronized from their hitbox and animation data, therefor this player now has an unfair advantage.

When the client has trouble receiving data from the server a method named Client prediction is used to simulate other object on the client’s end. Using the latest revived data payloads the client tries the predict what the other clients would do based on previous inputs.

The server however itself is still receiving data from other clients, and therefore a desynchronization can happen as the client’s prediction can differ from the information on the server. To solve this issue the server and client try to reconcile and find a proper middle ground, or the server completely overwrites the client if the prediction was way off.

Implementation

The implementation turned out to a bit of hassle, but I got it done.

The inpuy payload to send input data to the server. And a transfrom payload to move the player.

Two payload classes were created to hold infromation that would be exhanged between clients and the server. Every frame a new InputPayload is created to be send to the server to be prossed. Meanwhile a Transform Payload is created to remember the player’s position. These values are saved and stored in 2 local arrays to be remembered for later.

In a game running at 60 frames per second and a buffer_size 0f 1024, these arrays will remember the last 17 seconds worth of inputs. Like tape these arrays will overwrite itself with more recent data should space run out.

The HandleMovement function is now called twice. The ServerRpc call sends the input data to the server to be processed. The non-ServerRpc function is called on the client itself so that the player can still move locally.

This variable hold the most recently porssedesed Transform Payload for a player

This is the code to move other players on the local client’s computer. This takes the serverTransformPayload values and moves the other clients accordingly. In case of connection problems it will simply continue based on the last record values.

This code runs whenever serverTransformPayload is updated. It compares the client’s calculated position and its current position and if there is a notable enough different, it will move the player back to it’s position on the server. I do want to point out that simply basing the need for a reconciliation simply based on position may not be a good idea as there might be more complex mechanics that could get lost this way. However it will work for now.

Now let’s jump ahead a bit and also apply synchronization to the projectiles that the players shoot at eachother. Unlike the players this is much simpler as the projectile has a very simplebehavior: they move forward in the direction they where shot at a consistent speed. Because of this a simple calculation can be done to determine where the projectile should be and simply move it it it’s not in the right position.

So problem solved right? Well, not exactly. ObvIously it would require a lot more work to properly keep players synchronized, more that I’m able to achieve alone in a short time frame. However there is another major problem that should also be addressed to have a good online experience.

Packet loss

The contents of a normal packet

Another problem when it come to online communication is packet loss. A packet is a small unit of data used to transfer data over a network. A packet loss occurs when for some reason or another a packet fails to make it to the target destination and becomes lost in the network. In the case of online video games this can result in player inputs not making it to the server. Some internet protocols do have built in measurements to handle packet loss but these are not suitable for most online games as they are usually slower. Therefore a custom solution must be created.

Continuing from the previously shown reconciliation code. The client looks through all the input it made since the last serverTransformPayload update and replay them from oldest to newest.

Further down we have this. During this frame the player shot a projectile. The client creates a new type of Payload with the necessary information to create a projectile, and sends it to the server.

I did notice a rare bug were sometimes two projectiles would be shot after a reconciliation. To fix this I created a list that would keep track what projectile calls did make it to the server. Only if the first call never made it would a new projectile be created.

With this I have some basic measurements in place to prevent packet loss causing player inputs to never be read.

Shooting bullets and collision

Shooting a bullet

Now it was time to get started with with game logic. This would only be possible if the players could shoot bullets and score points. Normally when I shoot a bullet in a game I create the buller right there. Both of these things are simple for a single player game, and are some of the first things I’ve learned.

However when it come to online multiplayer new challenge arrive. Creating this bullet does take up some memory and when developing a online game a developer wants to reduce memory usage as much as possible. To save on memory the object pooling pattern is used. With object pooling all the objects are created at the start and the game recycles those objects thought the game.

Spawning a bullet
Quece up

When the server is started all the bullets are created and put in a queue. When the player shoots a bullet the bullet that’s at the start of the queue is called and teleported to the right location. The bullet that immediately joined the queue again at the back of the queue. Just like joining a waiting queue in real life a queue works on a ‘first in, first out’ system.

Collision

Now the bullets to hit the other players. I could have relayed on Unity’s default collision system but this uses physics and I was trying to limit the amount of server load as much I could. To get around this I wanted to create a custom collision physics instead that shot a ray forward to determine if the bullet hit something. I would only so this collision check a few times per second instead of continuously to save on memory.

Me doing some tests on collision range

After testing however this system proved to be far too inconsistent to be used in game. With time beginning to run short I’ve decided to go back and just use a trigger system to check if the bullet in inside the collision checkbox of the player.

My custom made PlayerScore object
Adding a point to the currect team

It made sense to also start working on the scoring system. I created a custom object that held a teamId, the player controller that belonged to the object, and the score said player had achieved. This was a simple system that would allowed the game to save the score without keeping to much data in memory. It was important to remember what player belonged for collision checks.

The collision code. Each bullet is assigned to a team when shot. This was done in case I wanted to add teams in the future and because there was a rare chance a bullet would intersect with the collision hitbox of the player that shot it. Should that happen the bullet will simply ignore that collision.

I ran into one final issue that the collision would be registered on every single client and therefore every client would send a message to the server and give the attacker multiple points at once. The second check checks makes sure this message could only be sent from the client to bullet belonged to. I wasn’t sure if this is a perfect solution but it will have to do in the time I had left.

Score counter
A slider to indicate when the next bullet is ready

Game logic

Victory

Now that all of the game elements were in place it was finally time to start working on game logic. Doing the collision I thought ahead and already added a scoring system. Detecting a victory was eazy as I only had to compare the newly achieved score to the victory score.

The Gamemanager now knows when a player has won and now the rest of the gameobjects have to know what they needed to do depending on the current gamestate. I used Unity events so that I could call a event when the gamestate changed, and all object subscribed to this event would behave correctly. I created a parent script that has functions that would automatically subscribe to these events. If I wanted to have an object do something on a gamestate change I simply would inherent from that script and overwrite the function.

A grace period

Inspired by Team Fortress 2 I added a grace state. During this period players can join and play, but the game itself hasn’t started yet. This system gives slower connectiona more time to join the server before the game begins. For my game this is extra important as the game design states that a player cannot (re)connect to a server once a game has started.

With all this in place I was able to play a full game from start to finish.

Looking back

This project was tough. I did manage to achieve the goal I set for myself but it wasn’t easy getting there.

Like I said I did finish the project. I learned a lot about making online games and have a deeper understanding of how internet connections work. I do believe that if I really took my time I could finish this game into a full project.

I did underestimate the amount of work I needed to do. originally I had hoped that Unity networking would do everything for me and I only had to work on the gameplay, but that was a unwise though. There is a lot that comes to seamless online gameplay experiences and I now respect every online game a bit more.

The project took longer that I had hoped because I just couldn’t get the synchronization to work right. It was only a decent while that I learned that Unity Networking has its own basic synchronization functionality that was overriding my own code. It was only when I turned this off I could finally start making some good progress.

However despite all the physical and mental stress I’m happy with the project. Originally this page ended witha post mortem on why I wasn’t able to finish the project but now I’m writing on it’s completion. Sometimes the destination is worth the journey.

For anybody who’s reading this and who’s interested to making their own multiplayer project: keep it simple. Don’t make anything involving physics or coplated movement. it adds a lot of work you are probably not prepared for yet. Learn the basic and when you understand them, go grazy. Maybe you are writing a blog like this one day.

In the meantime, this is where I get off.

Thank you for reading.

Sources

  • https://betterprogramming.pub/real-time-game-server-internals-basic-theory-architecture-optimization-auto-scaling-b2070aa803d9
  • https://docs.unity.com/ugs/en-us/manual/relay/manual/introduction
  • https://developer.valvesoftware.com/wiki/Latency_Compensating_Methods_in_Client/Server_In-game_Protocol_Design_and_Optimization
  • https://www.gabrielgambetta.com/client-side-prediction-live-demo.html
  • https://gafferongames.com/post/what_every_programmer_needs_to_know_about_game_networking
  • https://www.techtarget.com/searchnetworking/definition/packet-loss
  • https://www.gabrielgambetta.com/client-server-game-architecture.html
  • https://www.oodesign.com/object-pool-pattern

One thought on “Mutiplayer tank game

Leave a Reply

Your email address will not be published. Required fields are marked *