Client-Server Netcode Architecture for large Multiplayer Games
Whether or not you choose to use all-int-one netcode solutions, or poke into low-level transport libraries, you end up realizing that the learning curve between “single player” and “medium-to-large multiplayer game” is very steep and different. That’s why, converting your offline-singleplayer game to multiplayer, is as hard as rewriting it all from scratch!
I hope this article will serve as a bookmark for best practices and approaches you need to know. before you event write a single line of code. If you’re not familiar with computer networking basics, I highly recommend you to check out Netcode 101 video https://www.youtube.com/watch?v=hiHP0N-jMx8
And once you figure out how the data travels through network, just resume this video and we continue on.
Let’s imagine that we want to implement high paced shooter with large scale multiplayer in mind. Suppose we want it to be similar to Battle Royale type of a game, with a big map and a player count between 50-100 in a single match.
With an idea of this scale, we quickly abandon any peer-to-peer kind of hosting, coz we need our game to be competitive or at least fair. So, first thing we need to learn is – “don’t trust the player”! Always assume the worst! – that players will try to cheat.
The solution is kind of intuitive – we make all game logic and calculations run in the central server, and make clients feel like they make everything happen on screen, while in reality they just send an input.
This is what we call – Authoritative server.
For example, you don’t trust the client with the health of the player. A hacked client can modify its local copy of that value and tell the player it has match more health that he actually does. But the server knows it only has small amount left – when the player is attacked it will die, regardless of what a hacked client may think. Same thing goes with client position in the world, shooting, casting a spell and pretty match any other interaction which somehow involve other players.
To summaries: the game state is managed by the server alone. Clients send their actions to the server. The server updates the game state periodically, and then sends the new game state back to clients, who just render it on the screen.
But wait, this all seems like a lot of data to transfer… Imagine a map with 100 players all running around and constantly interacting with each other and the server needs to keep them all up-to date at any given moment, otherwise making the experience worse for the players.
And so, we stumble on another question: how do we deal with latency and bandwidth problem? – First, by choosing the right network protocol.
/(https://habr.com/ru/company/oleg-bunin/blog/461829/) for pics/
In case of fast paced multiplayer, the winner clearly is UPD! And the big reasons why, is that every TCP packet comes with significant overhead compared to UDP, but most important is that TCP creates “Head-of-line blocking” problem. And it is BRUTAL to FPS multiplayer experience.
And when the question comes between reliable or fast network protocol – the answer lies somewhere in middle. or not we and up implementing our own reliable version of UDP protocol…
There is still a couple of things we need to know, if we want to squeeze maximum out of server bandwidth. This is where “delta compression” and various Serialization Strategies come handy. The whole purpose of this is to put as match information into smallest number of bits.
Let’s assume that we tackle all the problems mentioned above, and implemented our own network protocol solution. We configured out dedicated server, created some basic movement logic. Strap up simple client that renders and Animate out character. We connect from client to server and press some movement input. And then, after about 200ms, out character start to actually move…
This perceived lag between your inputs and its consequences may not sound like much, but it’s noticeable – and of course, a lag of half a second isn’t just noticeable, it actually makes the game unplayable!
Client-Side Prediction, Snapshot Interpolation and Lag Compensation is a solution for this kind of problems. But for all these methods to work we need to assume the game logic is deterministic enough. Meaning that the same logic we use on the server for moving character, for example, would work exactly the same if we use it in clients.
The process of client-side prediction refers to having the client locally react to user input, before the server has acknowledged the input and updated the game state. So instead of waiting! for the new Game State, to start rendering the inputs we send, we can render the outcome of that inputs, as if they had succeeded. While we wait for the server to send the “true” game state – which, more often than not, will match the state calculated locally.
With this approach implemented, the user will have offline-like game experience! And even if the latency to server is high, the game would feel responsive and lag-free!
We still need to synchronize local prediction to the actual server response. Bear in mind, that the Server is who in charge here. So, if we move our character and the server response was that we cannot do this. We need to correct out local position to the data from server. And this could lead to bad player experience, when player sees as character moving and then all of the sudden teleports back. But let’s consider the more often example… What could actually go wrong is – client can move character in two different directions before the server approved the first one. This leads to synchronization issues. And the way to fix this, is to realize that the client operates in present time. While the server response data is actually the state of the game in the past. And by the time server sent the updated game state, it hadn’t processed all the commands sent by the clients.
We can work around this problem by assigning an id, or a sequence number, to each request that we send. And when the server returns a response, it includes the sequence number of the last input it processed.
So, moving backward to our previous example with player inputs. When the client sends first input, we give it id of one. And the second input would come with id of two. After we receive response from server, all we need to do is compere game state with matching sequence number. And this approach should cover basic single player needs!
But, after all, we talking about multiplayer game. With dozens of players interacting with each other. Sure, each client can predict his own action, based on player input. But what do we do with other clients? How can we predict and move enemy players smoothly, without knowing their game state for about 100-200ms.?
The short answer is, we can’t… in real-time. But what we Can do is show other client positions slightly in the past time. So, if the latency between our client and server is 100ms. We can show the state of other clients with exactly 100ms delay.
Server Update Cycles
Now let’s take a minute to sink it in our minds, and talk about server update cycles. Considering we have on our hand fast paced multiplayer with tons on clients playing simultaneously. Imagine if server updated the world state every time it received a player input. It would quickly consume too much CPU and bandwidth!
A better approach – is to queue the client inputs as they are received, without any processing. Instead, the game world is updated periodically at low frequency, for example 10 times per second. The delay between every update, 100ms in this case, is called the time step. In every update loop iteration, all the unprocessed client inputs is applied, and the new game state is broadcast to the clients. So, by this example, every client connected to the server receive game state update exactly every 100ms.
When we receive Game State update on the client, we interpolate the local data to server data. And by the time interpolation if finished, the new data should arrive.
Remember when I told about the player sees itself in the present but sees the other entities in the past. However, seeing other entities with a 100ms delay isn’t generally noticeable. But what if the player shoots another player, as he sees him on the screen, and by the time server receive information about the shot, the player which is being shot already moved out of the way. After all, were basically shooting in the past…
This is where Lag Compensation technique applies. It is not a perfect solution to this problem. But it is a pleasant solution for most players most of the time.
And here’s how it works:
* When you shoot, client sends this event to the server with full information: the exact timestamp of your shot, and the exact aim of the weapon.
* Here’s where the magic comes in. Since the server gets all the input with timestamps, it can reconstruct the world at any instant in the past. What that mean is, it can reconstruct the world exactly as it looked like to any client at any point in time.
* This means the server can know exactly what was on your weapon’s sights the instant you shoot. It was the past position of your enemy’s head, but the server knows it was the position of his head in your present.
* The server processes shot at that point in time, and updates the clients.
With this approach everyone is pleased except for your target. That’s the tradeoff you make. Because you shoot at your enemy in the past, he may still receive shot a few milliseconds after he took cover or hide behind a wall.
There is many topics left to cover:
How do we manage client and server being deterministic, particularly the physics engine, which is often used random numbers for collision calculation?
We also run into problem of Floating-Point Determinism. simulations use floating point calculations, and it is considered very difficult to get exactly the same result from floating point calculations on two different machines. People even report different results on the same machine from run to run, and between debug and release builds. Other folks say that AMDs give different results to Intel machines. I will link the articles about handling this various problem in the description.
To summarize, I think it is important to know how modern netcode is working, even if you planning on using high level network solutions which may include the same principles I show you.