How to add network multiplayer to your indie game
Kinguin Summer Deals

How to add network multiplayer to your indie game

Blog

I recently competed in a game jam and used an open source game engine. I am considering using the engine in the future for a larger project but it is seriously lacking network multiplayer. I have been thinking about contributing multiplayer code to it and what work that would take. I have a separate ongoing multi-player HTML5 game engine project in which I have already built multiplayer functionally, so it’s fresh on my mind. I am a hobbyist indie game developer and look forward to any pro tips to help refine my mental model.

To build a multiplayer game myself I had to track down and piece together all this disparate information. I regularly work with many indie game developers who think multiplayer functionality is black magic and unobtainable to mere mortals. In my opinion many “full service” game engines have anti-multiplayer patterns baked into their learning materials and default patterns. This leads to developers following the path of least resistance through anti-multiplayer code patterns. Hopefully this guide can act as a holistic overview of what effort is needed to make your game multiplayer.

This post is high-level and doesn’t assume an engine, language, or framework. The architecture suggested is a client-server model in which client-side prediction is used with an authoritative server. Sometimes client-side prediction is also known as “rollback”, specifically in the fighting game community. This architecture is suitable for real-time action games, first-person shooters, fighting games, action RPGs, and other real-time simulations. The client-side prediction and authoritative client model is intended to prevent cheating by appointing the server as the source of truth.

Don’t build local multiplayer first

A trap I often see indie game devs fall into is that they want to build network multiplayer and they think building local multiplayer is a shortcut. This is a fallacy. What happens is they build out all the local multiplayer logic and simulation code with bad practices and caveats, we will discuss below. They have now painted themselves into a corner. Adding network multiplayer becomes a costly rewrite at that point. If you want to build local multiplayer, plan for networked multiplayer first.

Fix your game loop’s timestep.

First and foremost. You should be doing this even if your game is not multiplayer. A fixed timestep loop ensures that your game runs at a consistent frame rate. Most default loop mechanisms do not account for variable CPU processing time. So individual loop ticks may vary in the time they take to complete. This happens if your CPU decided to run some other background task while your game loop was ticking or maybe nothing intense happened that frame and your game loop completed sooner than expected. This is undesirable because your simulation logic will end up differing each frame. This gets especially bad when you add the server and multiple clients. Each simulation on each node may play out differently because frame ticks may vary.

This task is critical because a fixed timestep game loop ensures that your game has deterministic behavior. You can guarantee your game simulation will produce the same results based on the same set of inputs at a fixed rate per loop tick. If your player presses the move key and you have a fixed timestep you This makes it much easier for clients and servers to stay in sync. This ensures that the client and server will step in the same increments. A fixed timestep means that your loop will wait an allotted amount of time per tick. If you want a 60fps loop this will be 16ms per tick. A fixed loop will carry over extra time.

This gaffer on games article is the definitive fixed timestep implementation guide and dives deep in to the code for a fixed time step loop.

https://gafferongames.com/post/fix_your_timestep/

time deltas vs fixed timestep

You may already be using delta time updates in your game loop and be wondering if that is sufficient or equivalent to fixed timestep loop, it is not. Delta time will help your loop smoothly compensate for varying tick times but it will not be deterministic. If you take a set of frames, and you run that same set of frames multiple times with same user inputs. It’s possible that the delta time will vary at different points during each set run so that a direct comparison of each run will have different results. This then becomes problematic when you try and run the same set of frames between server and multiple clients. A Fixed timestep loop is essentially a ‘fixed-delta’ loop. A fixed loop will keep a persistent accumulator of delta time

Delta time vs fixed timestep

Decouple rendering code from simulation code.

You need your game to run headless. Headless means your game will run as a background process that will not directly output anything or accept input. A multi-player game will run the same simulation on a client and a server. The headless server is going to act as a black box that runs the simulation of your game world and you connect to this black box over the network. You want to reuse your game logic code on the server so that you have a consistent deterministic simulation between the client and server. The server is not going to be drawing anything to a screen. It doesn’t have a screen. If your game logic code is tightly coupled to drawing and rendering API’s You will not be able to reuse that code for your server because it will not have the drawing APIs.

headlessloop.png

In my opinion this is the biggest hurdle for most indie developers adding multi-player to their game. Many full-featured game engines assume you will be rendering something and tightly couple rendering with logic updates straight out of the box. These engine’s tutorials and learning tutorials may even demonstrate these non-multiplayer practices. Once you have built game logic and content around a rendering engine, it can be a large effort to decouple and extract it.

Now if your game simulation is running headless, and it’s a black box that you only update over the network, how will it know about input to update players’ positions? This brings us to our next point.

Extract player input into ‘Events’ or ‘Actions’ messages.

Similar to decoupling your rendering code from the game simulation, you also need to decouple input handling. When a player hits a button or initiates mouse input this should create an abstract event like “Player pushed move button”. These events don’t have to be directly tied to player controller inputs, they can be tied to anything users have control of like equipping or removing inventory items. The event message immediately gets sent over the network to the server and simultaneously applied locally on the client. This means you need mechanisms for publishing input events, and for listening to and handling input events. The listeners will subscribe to the input events and then modify the game state or player state as expected.

input-handling.png

Make event payloads dumb.

The event payloads should have limited information. This is to prevent hacking where players manipulate events before they are sent to the server. For example, consider a “Player moved” event. You don’t want an event with attached coordinates like “player moved to X,Y position”. This is a vector for hacking as a player could just set their own coordinates and teleport to wherever they want. You want the server to make the decision on where the player moved to. The server will receive a generic “Player pressed moved” and based on the servers know direction, the player it will start moving it at a velocity until it receives “Player stopped moving”.

Event based vs polling input models

This can be another challenging re-factor task for indie developers. There are two main patterns of input handling, event-based and polling. Event-based input handling can easily be adapted to generate and send the network payloads. Event-based input means when a player pushes a button an event object is created that you handle to update the player’s state. These input events are then serialized to sent over the network to the server.

With an input polling pattern you check the input state on each frame. As you process the frame, you check what buttons are down and update the simulation logic accordingly. If you poll and the player’s move button is down, the player’s velocity is updated based on that input state. This pattern is more work to adapt for multiplayer. Your client code will be polling the input state on every frame. You don’t want to send the input state across the network every frame. This high volume of network requests would be problematic. Network requests are not free on CPU resources and network bandwidth usage can be limited or expensive depending on your server host.

If you’re forced to adopt a polling input system to multiplayer network events you have a few options. You can build a new mechanism that tracks the deltas of polled input states. This delta tracker would trigger events when the polling state changes. So you would know when an input event was triggered or released. Another option for networking polling events is to build a secondary “input loop”. The input loop will periodically send the input state to the server. The “input loop” can run at a lower frequency than the simulation loop

Add networking code

Networking code can be intimidating and opaque to newcomers. A lot of newcomers immediately get hung up on thinking where and how they will publicly host their server. Do not worry about hosting a public server until your multiplayer works on a local network. You should develop your multiplayer game with the client and server hosted on your localhost development machine. Once it is ready it will be easy to move to a remote public server. Networking code is pretty dumb, think of it like plumbing once it is established you just use it to move data from one place to another.

Serializable game state

Any data that will be sent over the network needs to be serializable to a binary payload. Input events from the client need to be serialized and sent to the server. The server needs to serialize the entire snapshot of your game state to send to all clients. You should have your game state structured in a manner that is optimized to quickly be serialized to a flat binary payload. Hopefully you already have a mechanism in place for saving or loading games that can be re-purposed for network serialization.

Add client and server sequence numbers

The client and server need to sync changes. The client will need to store a history of input events for rollback purposes. The server will periodically send state snapshots back to the client. These snapshots will have a record of the last input id that it had received and acknowledged from the client. The client will hard reset its state to this server snapshot. If the server snapshot hadn’t acknowledged the latest known input id from the client. The client will replay its inputs that have occurred since then. The easiest way to do this is to add incrementing counter ids or sequence numbers to your frames and inputs.

Pick a multiplayer networking protocol

Most high-performance AAA multiplayer games use a UDP networking protocol. They use UDP because it uses fewer system resources compared to TCP/IP. UDP does not have built-in mechanisms for ensuring that network packets arrive and arrive in order. While this is more fundamentally performant, it will require additional algorithm logic to handle cases where packets arrive out of order or not at all. I have heard that with the evolution of Internet backbone infrastructure UDP has become more reliable over the years and you can almost get away without handling the failure cases. Take this with a grain of salt though and evaluate for your purposes.

Websockets is a popular networking protocol with robust libraries available in many programming language ecosystems. Websockets are built upon HTTP, which is built upon TCP/IP so unfortunately they have more resource demand than a raw UDP protocol. They are a reliable protocol so you don’t have to worry about compensating for dropped or missed packages. Websockets can be easy to install and great for game jams and prototypes.

WebRTC is an emerging networking protocol that was developed for high-performance peer-to-peer video chat communication. WebRTC has gained traction as a gaming protocol because it has the option to switch into “unreliable mode”. The unreliable mode will make WebRTC behave like a UDP protocol and bypass any retry and ordering logic for packet data giving it a performance boost. There are WebRTC client libraries available in most modern programming languages. Unfortunately, WebRTC has an awkward and cumbersome handshake mechanism that needs to be executed to establish a connection. This handshake requires additional server instances to handle and can be costly for an indie dev to maintain.

Multiplayer networking rollback and reconciliation.

Finally we get to the real meat and potatoes algorithm of networked multiplayer. I am going to do my best to offer a quick high-level look at the steps to do multiplayer state management and then offer more technical resources.

  • Client captures user action events, updates its local state, and simultaneously sends actions to the server.
  • Server listens for user actions and updates state.
  • Server sends snapshots of game state back to clients
  • Clients receive server state and reset their local state to match the server state snapshot.
  • Client checks its sequence of actions and replays any actions that have happened since the last server snapshot

There is a detailed and comprehensive explanation of how fighting games use rollback netcode on arstechnica I recommend checking it out. It has animated gifs demonstrating the rollback logic and does a more thorough explanation of it than I will be able to achieve.

Gabriel Gambetta has a series of posts on client server game architecture that is one of the best resources available and commonly shared around as the guide to building client-server multiplayer.

These resources will get you 98% of functionality. The remaining points are custom logic to match your game’s unique game-play and style. This is similar to how you have to tweak your physics or collision simulation to match your assets and overall game look and feel.

Make more multiplayer games

The majority of my memorable gaming experiences have been multiplayer experiences. I partly believe this is because memories are reinforced when you hang out with someone and you reminisce “hey remember that one time…”. This is impossible with single-player experience. I have been enjoying online experiences since at least Diablo (1996) and it feels like they have sadly, not yet become a norm, or degraded in quality. The recent huge success of Fall Guys shows whats headed in this direction. I’ve seen a lot of radical experimentation and creativity applied to single experiences from indie devs. I would love to see that also applied to multiplayer games.

Hopefully this post has illuminated some of the unknowns about multiplayer implementation. If you’ve read this and feel like it sounds like a big chore, I understand, consider it for your next project. I feel like there is a failure of the available tools and learning resources. Imagine if game engines were multiplayer by default. If they followed and enforced the patterns we discussed above. Maybe that will be the case for future tools.

Further resources

The following are some more hands on and detailed resources that dive in to the specifics of developing