Multiplayer Development Basics
Understanding Multiplayer - Part I
Multiplayer is a complex topic. I myself avoided digging into it for a long time. A big problem is that many tutorials you find online dive into the details too quickly. Not enough time is spent explaining the topic from first principles. With a basic theoretical understanding of the kinds of problems and questions that come with developing multiplayer, all the frameworks, options, and tools make a lot more sense.
That's why, in the following article series, I'll try to break down what I've learned on this topic as clearly as possible — hoping it might make getting started easier for others too.
What Is Multiplayer?
Synchronizing a Shared Reality
In short, multiplayer is the synchronization of a game state over a network across multiple clients. The game state contains all the information about the objects in the application that need to be synchronized, such as position, rotation, and scale.
If two people establish a connection and one of them uses an input device to move an object in the virtual world, changing its position, that change has to be communicated to the other person's application. That said, this doesn't mean that everything that changes in the application needs to be synchronized.
Developers should be aware of whether an object falls into one of the following categories:
- Continuously synchronized objects
- Event-based synchronized objects
- Non-synchronized objects
The goal is to keep the amount of data that needs to be synchronized as small as possible. If an object, even a dynamic one, has no effect on the gameplay loop, then it doesn't need to be synchronized either.
EA Sports FC 24 (Electronic Arts)
Take the game EA Sports FC 24 as an example. Actors in the game, such as the goalkeeper (A), fall into the category of "continuously synchronized objects." Alongside the goalkeeper's position, all players should receive every other piece of data necessary for gameplay. This information has high priority and should be updated every frame, so that all players can base their decisions on reliable data. If the goalkeeper's data were only updated every 200ms, for example, a game of this kind would be impossible.
The score (B), on the other hand, doesn't need to be updated over the network multiple times per second. Goals happen every few minutes. That's why synchronizing this object falls into the category of "event-based synchronized objects." When a goal is scored, the score is updated and all clients are notified.
In the example, the crowd in the stands (C) is animated in the background. They cheer, move around, and contribute to the atmosphere and immersion of the game. However, it doesn't matter for gameplay whether fan #8572 in the background is currently throwing their arms up or waving a scarf. Objects like this fall into the category of "non-synchronized objects." They can differ from client to client without any impact.
As you can see, it isn't necessary for the entire application to be synchronized. The game state isn't everything that exists in an application — only what's necessary for gameplay.
Function Follows Design
Simulations and games are typically immersive, highly interactive products that require constant testing to ultimately succeed. In the course of development and testing, new ideas emerge and requirements change as the project moves toward the best possible product. It's an iterative process that leads to the goal. In short: gameplay keeps changing and gets continuously readjusted.
But as we learned in the previous section, essential decisions about which multiplayer features need to be implemented depend on the gameplay. Implementing a multiplayer feature requires committing to something beforehand. Not only that — the way the application works also determines which infrastructure solution, network topology, and security measures are necessary. Switching approaches later is time-intensive and should absolutely be avoided.
This creates a conflict between the iterative, usually agile nature of projects like this and the need to commit early to certain constraints in order to build solid multiplayer. There's no simple solution here, only compromises you have to choose between. Pre-built tooling ecosystems that cover the entire multiplayer use case and, thanks to a proven API, offer the ability to switch between solutions, help enormously — but depending on the product, they can become costly and raise new questions around data protection and data security. Developers therefore have to come to terms early on with the fact that integrating multiplayer will slow down and constrain the development process.
Authority Over the State
Besides the question of which objects need to be synchronized, you also have to ask who has authority over an object and determines what the "single source of truth" is — either a client or the server.
By default, multiplayer SDKs take a server-authoritative approach. This means only the server can make changes to the data that needs to be synchronized. When this data is updated by the server, it's sent out to the connected clients. But if all synchronized objects were managed by the server, there would be no interaction from the clients at all. That's why, in the vast majority of cases, user input is continuously synchronized and treated as client-authoritative. This means input data — such as which keys are pressed — can be changed by the client. The server, which holds authority over the position of the avatar being controlled, processes the input, updates the position accordingly, and sends this new data to the clients.
Besides the advantage of preventing cheating, server-authoritative also has a serious drawback. Communication between server and clients always has some amount of latency. A client with a connection latency of 500ms has to wait 0.5 seconds before their own avatar reacts to a key press. Depending on the use case and gameplay, this can lead to frustration. In general, SDKs provide plenty of tools for managing state and authority over that state. For example, the latency problem mentioned above can be resolved with client-side prediction or dead reckoning, both of which are offered by some SDKs. The available tooling landscape will be covered in more detail in a later section.
Misconceptions
Before we get more concrete and dig into the topology and software that multiplayer applications may need, it's important to address some misconceptions. With the many resources available on this topic, it's not uncommon for a mistaken understanding to take hold.
Simulated on the Server.
You often come across this phrase alongside things like "server sends back..." or "server manages data...". This can create the mental image of the server as a kind of database that manages different data sets via IDs. But in reality, the server is nothing more than a special build of the same application the clients also use. It runs without a GUI — headless, so to speak — but performs the same calculations as the clients. The big difference is that, by default, the server has authority over all synchronized objects and represents the "single source of truth," so to speak.
Server Builds.
As just mentioned, a special build of the application runs on the server. It's important to understand that this is a build of the very same source code the clients also use. There's no separation between frontend and backend, as you'd typically know from classic software development. Server and client code exist side by side and often share the same classes. While this can be confusing at first, it's necessary — otherwise certain tools for synchronizing game state, such as remote procedure calls (which we'll cover later), wouldn't be possible.
A First Overview
In this first part of the multi-user series, we covered the fundamental understanding of an application like this and how the multi-user feature is influenced by existing features. We also looked at the challenges that inevitably come with a multi-user feature. In the next post, we'll go further into detail and take a closer look at the infrastructure and software landscape around multi-user development.
