How to create a basic Multiplayer FPS
By: Charlie Yau
For my research I decided to go with something I was really curious about since I ever started developing games. You might have guessed it from the title or the first image but by no surprise it’s a First Person Shooter Multiplayer game. The reason for that is because I grew up as a child playing these games. I can definitely say that I had a lot of fun playing them. Having the chance to look at the ‘backside’ of these games was for sure a learning experience.
This report will cover the steps on how I created a Multiplayer FPS game. In the last chapter I will reflect on the results and talk about future references.
My goal was to create a Multiplayer FPS game that has Client Prediction, Reconciliation and Lag Compensation. This is what a basic FPS game should have.
Table of Contents
- Networking Basics
- Netcode Framework
- Project Startup
- Client-side Prediction
- Lag Compensation
1. Networking Basics
To start off and create a multiplayer game you most likely need to understand the basics of networking. One of the most important things of networking is to understand that we are dealing with multiple connections.
These connections or as we can call them clients, have to synchronize in order to give the clients the same playing experience. One could say that the clients could communicate directly with each other (‘peer-to-peer’). But for a fast-paced multiplayer game you could argue that a cheap solution as peer-to-peer is unacceptable. Things such as host advantage where the host has a ping of 0 and is the top scoring player or where the game is solely dependent on the stability of the host.In the end it all boils down to the infrastructure and how many people will play it. That is one of the reasons I opted to go for a Client-server architecture. Not only do I wish to present this game to a small number of clients, I also want to prevent cheating. Now I know that a client-server architecture won’t prevent cheating, but it definitely makes it harder because it covers most of the cheating problems.
So by having a ‘Main Server’ we can serve the clients. The main server is different from the clients. It is not meant to be used directly by any of the clients it also should have the best connection and preferably an uptime of 100%.
In the image down below you can see that Client 1 presses the ‘W’ key. This key press will then be processed into a packet, which we will send to the server. The server receives this packet and executes some logic to get a result out of it. The server will then send this result back to the clients. The clients receive the results and are now notified by the changes of Client 1.
It is important to understand that basic networking is all about two kinds of communication.
- A client sends a message to the server.
- The server broadcasts the results to the clients.
2. Netcode Framework
Starting off my research the most important thing was to decide on a netcode framework. Surprisingly enough Unity offers a lot of options when it comes to netcode frameworks. To pick one framework I had to come with a set of criterias:
- It needs to be free (student life)
- It needs to be Low Level
- It needs to have good scalability and performance
So the list I could choose from was:
- DarkRift 2
Mirror is a high level networking library for Unity. It’s extremely easy to use and it’s also the reason why some small indie developers use it.
Low Level API: No
Good Scalability & Performance: Yes
Photon is a Unity Package for multiplayer games, offers a lot of features and has its own dedicated Photon Server(s).
Low Level API: No?
Good Scalability & Performance: No
DarkRift 2 is a networking library that is praised for its speed, lightweightness and sheer power. Has Bi-channel communication (UDP & TCP) and also supports standalone console apps etc.
Low Level API: Yes
Good Scalability & Performance: Yes
In the end I decided to go with DarkRift 2. Because it is fast and the overhead of UDP packets is only 3 bytes. It also is Low level which gives me more control when making a multiplayer game.
3. Project Startup
When developing a multiplayer game it’s really important that we don’t have to upload our build to the cloud server for every change of code or to test a feature. Instead, it’s better to have a server running on our own computer. This way we can test locally which is ultimately much faster. So my first step was to create a local environment.
First of all we need two projects. One which will be our Client and the second is our Server.
I created the two projects with Unity 2019.2.6f1.
In order to connect to the server we will need to write some code and attach some scripts to a gameObject. I created a gameObject called “NetworkManager”. I dragged the Unity Client script onto the object. With this Unity Client we can connect to a server. As you can see there are two fields (IPAddress & Port) which you can fill in.
Fill in 127.0.0.1 as the ip address, if you want to connect to your local server.
And for the port we are using the default port which is 4296.
When that’s set we need to create a script so we can handle the connecting, disconnecting and packet sending/receiving. For that I created a script called “NetworkManager”. To connect the client with the server we start our connection attempt in the Start method. We also added a callback function that handles what happens if we fail to connect.
When we successfully connect to the server we will send an event that will notify other classes. For example we can switch scenes when we actually get a connection with the server.
As for the server we create a gameObject again and call it “NetworkManager” as well. Instead of a Client script we attach a server script to this gameObject. The script is called XmlUnityServer.cs. Drag the script on the gameObject and fill in the config file for the server. To make things easy just use the default config file from DarkRift 2 called ExampleConfiguration.xml.
Once again we need to create a script called “NetworkManager” so that we can handle the events of clients connecting, disconnecting and sending/receiving packets.
To make sure that I have a clear view of what packets we can send and receive I made a PacketHandler script.
On Initialize the PacketHandler will make sure to subscribe to the ClientConnected & ClientDisconnected events.
When a client connects to the server we want to make sure that we are subscribed to its MessageReceived events. We don’t want to miss any packets sent from the client. We also want to make sure that we are not subscribed to that event anymore once the client disconnects.
4. Client side prediction
In the chapter of networking basics we covered that clients send input data to the server. The server then sends a result back. If we assume that we move everytime we get data back from the server we would have a decent amount of delay in between our key press and actually moving.
To prevent this we introduce what’s called Client-side prediction. Given a certain game state and the set of inputs that we send to the server we can predict where the player will be in the next frame. In the perfect scenario we would create our input data, use this data to move instantly on the client-side and then send the same input data to the server. So instead of waiting for the confirmation from the server we move as we send the data. This way we get rid of the delay.
But the thing is that we cannot assume that we always get a perfect scenario of how things will end up. So for instance if a client loses connection for a few seconds we will get synchronization issues. This happens because the client is probably still pressing the forward key thus moving forward. But the server doesn’t get the input data and will remain in the same position. For other clients you’re probably standing still, so this doesn’t affect the server state whatsoever because the server remains authoritative.
So what happens if we send our input data but predict wrong. Depending on how you wrote the code that corrects wrong prediction, you will basically snap to the corresponding frame and position. But this gets really annoying when you have a high latency. You’re not able to play the game, all you’re going to be doing is teleport back and forth. You move forward on the client and after moving you will get results back from the server saying that you were still standing still. So you move back to that position, but after a few milliseconds you get the ‘right position’ and snap back to that position. To fix this we will need to implement Reconciliation we will cover this in the next chapter.
This is the Client-side prediction code that I use for my multiplayer game.
- First I get the input, so key inputs and mouse movement.
- Then I convert this data into a packet.
- After that we simulate the movement
- Send the input packet to the server
- Enqueue the predicted result and input data into our history.
In the last chapter we noticed synchronization issues with client-side prediction. Where clients with high latency would teleport back and forth. This is where Reconciliation comes into play. Instead of just sending your input and predicting the results you also store these inputs and results along with the frame number. This way if we got a wrong prediction we simply playback the movement with our stored results and input. One thing to keep in mind is that you should clear your stored results. If you store your results up until frame 10 and the server sends the result of frame 5, which is also wrong, you don’t want to replay all these frames. So you clear the frames until 5 and then we replay all frames.
6. Lag Compensation
One of the most important things in a FPS game is that your shots are accurate. If you shoot a client in the head you should expect a frag. But in some cases you might not actually get the frag and it will result in a missed shot. But why do such things happen?
To break it down, from the moment that you shoot your weapon and see your muzzle flash appear, you’re basically shooting in the past. The client that you were trying to shoot is probably not there anymore. But luckily there is a way to fix this issue. Instead of just sending the input data of our mouse click we will also send the exact frame number. By doing so the server knows exactly when that shot was fired.
Now that we have this time stamp we can use this info on the server and process it. By processing this info we can confirm if your shot hit the enemy. To do this we need a seperate physics scene for our game. A Physics scene is basically another scene instance specifically for physics. So we can use this scene to reconstruct a snapshot. Just like the client, the server also stores the results. The server stores the whole game state, so it knows exactly where every player was standing on a certain frame. So all we need to do is get this data, set every client’s position to the correct position and test a raycast for this shot.
In the end I was able to create a multiplayer fps game that has client-side prediction, reconciliation and lag compensation. This was my goal and I am glad that I could reach this goal within the given time period. However I regret the decision to make a multiplayer fps game, because doing this within like 4 weeks isn’t fun to do at all. You’re forced to basically use a framework instead of writing your own framework and creating a standalone server is also not an option. But it was definitely a learning experience.
7.2 Future reference
I will remake this project in the future. But it will be a lot bigger and more advanced. For instance I will make a Master Server instead of having a Master Server and Server combined. This master server will send out heartbeats and retrieve public server and store them into a database. Client can then ask the master server for a list of available servers. As for the framework it will be a custom framework. The sole reason for that is because I want to learn more about creating my own netcode framework. As for the game server I will create a standalone application that can either create a local server or a hosted server. Other than that it will support custom map loading so that you don’t have to create all the maps before building the server. Think of it as BSP loading where map data gets read by the server and recreates everything during runtime.
Link to source code:
TwoTenPvP.github.io. (n.d.). TwoTenPvP.Github.Io. Retrieved October 15, 2020, from https://twoten.dev/lag-compensation-in-unity.html
Client-Server Game Architecture – Gabriel Gambetta. (2019). Gabrielgambetta.Com. https://www.gabrielgambetta.com/client-server-game-architecture.html
Source Multiplayer Networking – Valve Developer Community. (2009). Valvesoftware.Com. https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
Multi-Scene Physics. (n.d.). Unity Learn. Retrieved October 17, 2020, from https://learn.unity.com/tutorial/multi-scene-physics#
Lim, Q. W. (2019, June 5). How do Multiplayer Game sync their state? Part 2. Medium. https://medium.com/@qingweilim/how-do-multiplayer-game-sync-their-state-part-2-d746fa303950
(2018). EmbeddedFPSExample (L. Stamplfi, Ed.) [Review of EmbeddedFPSExample]. Introduction | EmbeddedFPSExample; LukeStamplfi. https://lukestampfli.github.io/EmbeddedFPSExample/guide/introduction.html
Sigatapu, N. (2019, October 28). Making a Basic Multiplayer Game. Medium. https://medium.com/castle-archives/making-a-basic-multiplayer-game-b919bc48d17a
DarkRift Documentation. (n.d.). Darkriftnetworking.Com. Retrieved October 26, 2020, from https://darkriftnetworking.com/DarkRift2/Docs/2.7.0/index.html