Creating a multiplayer, online game can be challenging, requiring the developers to anticipate latency and packet loss, consider what processes are authoritative, and selectively communicate data on the network so that it is not overloaded.
Despite this, multiplayer games are deeply rewarding, and bring many times more players to your game than those that only support a single player! Here, we’ll go over multiple methods and use cases to program a multiplayer game whilst minimizing artefacts such as jitter and lag.
It is important to note that the majority of these issues are only present in Online multiplayer, as local multiplayer is run on a single machine and process and does not need to worry about network connections! We will focus on solutions in Unreal Engine 5 and Unity, though these solutions are universal.
Now, why would you want to develop a multiplayer game?
In every case, clients, a.k.a. players, will connect to a server and interact with it throughout the game session. Broadly speaking, there are two configurations of network setups, the dedicated server model and the client-server model. This decision is often made by your genre and style of game, rather than an aesthetic choice!
In this approach, the server that clients connect to is an executable process with no graphical display upon a larger hosting platform such as AWS, Pragma, Epic’s own Online Services, or the like.
The server will execute gameplay logic and inform all connected clients of the game state without any computing time spent on visual effects or excessively complicated geometry. While this might save in processing speed, it is often the case that the server is its executable (.exe) that is run separately from the main game, by a player’s device–or more commonly–one of the aforementioned servers.
The main benefit of the Dedicated Server is that it persists when no clients are connected, and creates a “middle ground” for all client interactions, which have an equitable amount of latency to the server, rather than the single “host” being many milliseconds closer to the action.
Note: this approach requires some setup overhead that might make it less suited for first-time netcode programmers.
In this approach, the server is run simultaneously from a client’s machine and executable. The client will connect to their server and host through it, propagating game states to all clients that connect to it.
This model is easier to maintain and set up, but latency is unpredictable between connecting clients as it is dependent not on a single, stable server, and rather on fluctuating internet service providers’ speeds. There is no cost to set up or host this server, and the main benefits include speed of implementation, access, and connection.
The Client-Server model, however, has an upper limit on player count, as a single client has to simulate the graphics of the game whilst also computing and administrating the server for all connected clients. Additionally, it provides less fairness to all players, as one will always have a network advantage.
Generally, unless a game requires a 10+ player count, or wishes the server to outlive all client’s interactions with it, the Client-Server model is preferred.
It is an illusion that a multiplayer game is played fully together. In truth, devices at potentially incredible distances across the world are simulating the entire game in isolation, and the minimum amount of data possible is being sent between the server and its clients so that the game appears to have all players present.
These processes can get out of sync, causing enemies to appear alive on a client and dead on a server, commonly making them unkillable; they can provide irregular movement through variable network speeds, causing yourself or others to have extremely erratic movement speeds or patterns; they can open avenues for cheating, as clients can manipulate the data they send the server and, unless safeguarded against, perform actions such as multiplying their speed, refusing to die, etc.
Ensuring these disparate processes do remain in sync is the grand desideratum of multiplayer netcode.
By default, no data is communicated between actors on the network. Developers must carefully plan their game states, players, and game objects to replicate the necessary information and only the necessary information. Straining the network with massive amounts of information will only slow the connection, causing mismatched states or disconnections.
In Unreal and Unity, there are two primary methods of network communication, processes that are very similar in all other engines, and so these tips apply to a general case game, as well. Both will be used in a multiplayer project, though which to use for a specific feature can vary.
Individual variables, such as a player’s health, remaining ammo, and the time of day, can be marked as variables to be gathered for replication. In general, this approach is the default and is chosen when a variable must be changed and kept in such a state. In this case, a client that re-joins or experiences packet loss will still receive this state, just at a later frame.
- In Unreal C++, the replicating object or actor must be registered for replication with GetLifetimeReplicatedProps, and individual variables with DOREPLIFETIME.
- In Unreal Blueprints, the actor and variable must simply each be set to replicate.
- In Unity, the same is true with marking a variable as a NetworkVariable.
There are additional settings for determining when the variable should replicate, such as only if a client joins late, or on a much longer timeframe than 100+ times a second.
RPCs, or Remote Procedure Calls, are individual events that are fired immediately, with low bandwidth, between the network actors. These happen irrespective of the aforementioned variables’ replication state, and so can reach their network target quicker. They are not saved after fired, and are ideal for “one-and-done” effects such as visual effects, audio effects, single attacks, et cetera.
These are preferred for either immediacy or when you wish to change a variable and cause an effect in the same frame without delay, such as an event to kill a player when the server determines their health is 0.
Note: Unreal has RepNotify variables to perform functionality immediately upon receiving an updated variable from common replication, which can be substituted for many RPCs.
Player 1 presses forward on their controller. Player 2 shoots a rapid-moving projectile towards them, such as a gun, and their aim is perfect! However, at the same time Player 2 fires, Player 1 has already pressed back on their controller and stopped moving. Player 2’s game does not know about this yet, due to latency. In this case, the server will see the projectile as overshooting the now-reversing Player 1, and on Player 2’s screen, the projectile will simply pass right through their target without effect.
This is a very acute case, but one that can cause great player frustration with the netcode of the game, often without being able to place exactly what the problem is.
In truth, it is impossible to remove this problem without transitioning an online game to a local one, which is why e-sports competitions happen upon one local area network (LAN) in physical venues. There are, however, many ways that games reduce or eliminate player frustrations, at the cost of other systems.
By default, most games, either Dedicated Server or Client-Server models, put the server in the authoritative role. It determines where enemies are, what actions are allowed, the health of all actors, and so on. It also requires that its approval be given for all actions, meaning that for full replication, a player must press a button, which replicates to the server, checks necessary conditions, and then replicates the effects to all clients.
This adds two times the Round Trip Time of the network (2xRTT) in delay for every player action, which, unless all players’ network connections are quite good, will make the game feel unresponsive. The common solution for this is allowing the Client authority over their own character’s movement, actions, et cetera.
In the case of movement, this means that when a player presses forward, their pawn moves forward, and the server will simply accept and replicate the client’s actions. This can, however, open up avenues for cheating players, where they inject code into the client-side executable to send arbitrary speeds or positions to the server. If your game is competitive and not a friendly game between known friends, you will likely want to stay as server-authoritative as possible.
In such a case, the server can second-guess and double-check the actions the client performs, such as ensuring they do have enough mana to cast a spell before they do. The server will then inform the client to cancel its visual effects or animation, causing visual error, but remaining both responsive and server-authoritative.
Cost: Complexity, the possibility of cheating.
A common technique for movement is to evaluate player positions a few frames ahead of their position and assume no further input while allowing the client to control their movement. This allows less visual artefacting of shots hitting a target ahead of their position, yet can lead to visual artefacting if the game requires a rapid change of direction.
Correctly managing this is difficult, but is one of the more popular solutions due to the number of remedies for the aforementioned problems. Tutorials for client-side prediction are far and wide, and out–of–the–box solutions exist such as SmoothSync on the Unreal Marketplace.
Benefit: Fluidity of movement when actors are controlled over the network
Cost: Complexity, the possibility of teleporting players if badly managed.
Allowing the client to proceed with actions such as firing a gun, swinging a sword, jumping out of a spaceship, or killing an enemy, but validating that such action is both acceptable and within the game rules on the server, allows the server to minimize cheating without creating severe latency or jittery movement on the client.
When an action request is denied, the client will then update to match the server’s state. In settings of moderate to extreme latency, this can cause visual artefacts and undesired outcomes, and so ample testing is required. This method is ideal for visual effects not pertinent to gameplay, such as muzzle flares, weapon impacts, magical explosions, or the like. These make the game feel reactive, while the gameplay rules are still server-administrative.
Lastly is the subject of proper quality assurance of network conditions. A new developer might assume that they require multiple devices to test their multiplayer game, perhaps even dozens! They would not be entirely wrong, there is no perfect substitute for running the game on the target devices, however, in most modern engines there exist ways to simulate network conditions.
In Unity, you can use the Network Simulator Component, and in Unreal’s Advanced Play Settings, you can turn on Network Emulation and define exact latency and packet loss. Using these when testing a game’s netcode is vital, as many visual artefacts or outright game-breaking issues will arise when the latency is first introduced to a game’s simulation environment.
Finding solutions for these bugs often lies in ensuring the right variables are replicating at the right times, and using the previously mentioned remedies for latency to reduce visual artefacting.
All in all, online multiplayer games are simply more difficult than single-player games, but with these solutions, you can manage latency and variable replication correctly, and create the illusion that all players are in the same game, the same process, and perhaps even the same room!