Developing a multiplayer game is hard. Making a real-time multiplayer game is even more so. But why is that the case? In this post I will focus upon some of the major points of difficulty, and some of the common strategies one can use for overcoming these obstacles.

1) Players hate (the appearance of) lag

In reality, players don’t really hate lag. They just hate it when you don’t hide it from them. The industry-wide solution to the problem of lag in online games is to trick players into thinking there is none by use of some sort of client-side prediction. In a nutshell, this means that the player’s client attempts to predict the server’s future state of the world (or perhaps just a part of the world, like the player’s position) such that immediate feedback is given whenever a key is pressed or the mouse is moved. It should be noted that it is very helpful to have the client be running the same exact simulation code that you’re running on the server in order to minimize mismatches between the prediction and the eventual server state.

Lesson: If you’re not lying to your players, you’re doing them a disservice.

2) Players really hate it when “weird stuff” happens

Tears in the fabric of space and time can really put a bad taste in a player’s mouth. Take this scenario, for example:

John, seeing that his enemy is pointing a BFG-9000 directly at him, activates his temporary invincibility shield ability.

John’s screen goes black and sees that he has died to BFG-9000 fire. WTF?

This can easily occur due to the fact that there is inherent latency between John’s client and the server, and therefore his client may not know of events have already occurred on the server. At the point in time where he activated his shield, his enemy may have already hit him.

A reasonable way to resolve this issue is the following (note – this is how Halo: Reach implemented the “Armor Lock” ability):

Impose a short “charging up” period of time for activating the invincibility shield. So, when John presses the key to activate the ability, a cool animation displays on his client, only after which the ability is active.

Upon receiving the key press to activate the ability, immediately send a message to the server with a timestamp of when the ability should activate.

Lesson: There are cases where you might need to spend time to make time.

3) Certain gameplay mechanics are impossible without the right approach

I’ve had the following conversation quite a few times at this point:

DevDude55: Okay man, so, I’m going to make this, like, awesome multiplayer game. The basic concept is it will be a bullet hell game that will support, like, 100 players simultaneously, all fighting each other. Awesome, right??

Me: Wow, that sounds cool! How will you be keeping each player synchronized?

DevDude55: Well, my plan is to send the X and Y coordinates of all of the players and bullets periodically to each client. I like to keep it simple!

Me: Ahhhh, I see… good luck! 😉

The problem here is one of bandwidth. For moderately smooth gameplay, let’s assume we’ll be sending the player coordinates 10 times per second to each client. Let’s assume we use an extremely packed binary format for each game state snapshot, and each entity described therein is 3 bytes long:

1 byte for a gameplay entity unique identifier

1 byte for X position

1 byte for Y position

So that’s 3 bytes per entity, and we have 100 player entities, and we are sending this 10 times per second… That means approximately 3KB of data per client. That doesn’t seem so bad, especially if we employ some compression! But wait… We want to send the current health of each player (+1 byte). Well, now we’re at 4kb per client. And darn, we forgot that we want to the players to be able to accrue experience (+1 byte). 5kb… Oh, and wait! There’s all of those bullets flying around, too! Oh, shit.

In reality, bandwidth becomes a critical issue when attempting to support far fewer players than 100, especially if players have a reasonable amount of metadata (health, mana, level, etc). Luckily, if we are careful programmers, we can rely on using a deterministic simulation on each player’s client to circumvent these issues.

Basically, the thought is that we only have to transmit the input of all players to each player, and as long as they are entered in the exact same order, and as long as our simulation is truly deterministic, we should get the same result on each client.

Lesson: The simplest and most intuitive approach to your networking layer does not necessarily scale well. It WILL be a problem for certain types of games.

4) Performance can be a difficult nut to crack

One of the issues that I ran into myself during development of Star Sovereign was a result having to re-run the simulation of old game ticks when player events from the past were received. This happens all the time, of course, because a laggy player may have issued a command 300 milliseconds ago, and therefore that command needs to have run at that particular point in time. The real crux of the problem was the fact that my collision detection code was using quite a bit of brute force, and did not leverage any sort of temporal coherence. Simply put, this is the idea that over short periods of time, scenes do not change much, and you can leverage that fact by only detecting the changes from one state to another, as opposed to recalculating everything from zero. This is a wonderful post that explains how you can add persistence to your Sweep and Prune collision detection code quickly and easily.

Lesson: Keep your simulation code lean, mean, and performant.

5) Simple features? They rarely exist in multiplayer games

Writing synchronous code is easy and straight-forward. The moment you step into the asynchronous world, everything is different. You need to cope with interrupting events, unpredictable latency, and as stated in Section 2 above , rips in the space-time continuum. Features that might take 30 minutes to implement in a single-player game can be orders of magnitude more difficult to implement in a multiplayer game.

I think the best way to cope with this fact is to rely on a solid foundation to build your game. This might be using a well-written, well-documented multiplayer library like Photon for Unity, or rolling your own code that abstracts you (at least in part) away from the inherent asynchronicity you’ll be dealing with everywhere.

Lesson: Find the right tools for the job.

Conclusion

Anyhow, I’m sure I’ve missed quite a few important items of importance with this short list of potential pitfalls you might encounter while developing a multiplayer game. I may revisit this post in the future to add additional notes and corrections, but I do think it covers some of the common critical problems in these types of games, and potential solutions / workarounds to them. Feel free to post comments, suggestions or questions regarding any of these topics (or other related things).