Chronicles of the Development of a Multiplayer Game: Part 1

This series of posts tells the story of two friends who decided to write a multiplayer game in JavaScript: a top-down space shooter called spaaace. Amazingly they continued to be friends even after they were finished. This was not an easy task - and neither was making the game. You can play the game here. The physics & networking code was split off into a separate open-source project called Lance.gg.

I won’t be offended if you fork it. The code is available here.

I think the Death Star is just around the corner!

JavaScript? Are You Crazy?

There is already a good bit of literature on this topic, but less so from the JavaScript point of view, which is the reason I’m writing this post. Also, since the existing literature was written, some of the assumed characteristics of wide-area networking have changed. For example network ping times are impressively low, even close to the speed of light. Going halfway around the world from a client to a server can be done in less than 200ms, light would do that in 66ms. Bandwidths are also much higher than they used to be. All this should translate into a better game experience.

JavaScript is not an immediately obvious choice for writing a multiplayer game. Not by a long shot. Yet many factors contribute into making it a viable choice, among them the improvement of network speeds, the availability of advanced graphics libraries like three.js and PixiJS — which leverage technologies like canvas and WebGL. Our biggest concern going into this project was lack of UDP packet transport in the browser — we were concerned this would impact the networking round-trip time.

Spaceships Shooting at Each Other

The game we set out to make was the canonical, rudimentary, space shooter. Think of asteroids / star control. We called it spaaace. This is a great starting point, because it allows us to experiment with movement control, physics, real-time response, while creating a game that would be fun to play.

In our version, each player drives a spaceship in a two-dimensional, wrap-around world. The spaceship can accelerate, decelerate, turn left or right, and fire missiles at other spaceships.

Development of the Game as a Story

In these posts I cannot realistically describe the entire game. But I can describe the chronicles: the problems that we faced along the way and how we solved them. What were the tools we used, and how we made progress with each challenge. It occurred to me that the process of making the game is interesting in its own right. What is it like to write a multiplayer game? In what areas did we spend the most time?

There is a recurring theme in the process of developing a multiplayer game. The recurring part is that someone plays the game, and the ships (or missiles) start moving around in a strange or jittery way. I call this “poltergeist” as in: “Hey Gary, when I fire lots of missiles the game goes poltergeist.” This is the equivalent of going to a mechanic and saying that the car is making a strange noise. Or going to the doctor and saying that your tummy hurts. It basically tells us nothing. The symptomatic description is useless, so the analysis invariably required us to look at server and client traces and figure things out slowly.

Our game architecture relied on a technique called client-side prediction. This technique requires that each client try to predict what is currently happening on the central game server, and make corrections as necessary when new updates arrive from the server.

Because of this technique, each investigation process follows the same basic pattern, which narrows the problem down to a specific subspace. Is the problem visible on the server? If not, are the positions and state reported to the client in a timely manner and with reasonable values? If the client disables prediction, does the game work properly (if unsmoothly)? If the client prediction gets good values, then do we have a problem in the rendering process?

Challenge 1: Bending.

The Symptom: “the ship resists and pulls as I turn it”

Bending is the process of adjusting client positions to match server positions, ever so slowly. The idea is very simple to understand, but the actual bending configuration is not. There were several occasions of poltergeist which required an adjustment in the bending configuration.

What I learned:

Bending must be done incrementally, so that each game step applies some increment of the desired bending delta.

We defined bending as follows: the percent correction that the client should apply towards the server’s true value, by the time the next server update arrives. But bending is not just a single magic number, like 63%. Bending is a large set of fine-tuning parameters. You need to specify different bending factors for the adjustments of position, of velocity, and for rotation. All of those factors will have different values for different object types in the game. And all of those factors for all of those objects will have different values depending on whether the object is directly controlled by the player (a.k.a. player-owned), or whether the object’s position only changes when new data arrives from the server (a.k.a. remote).

In our game, the user’s own spaceship would sometimes “resist” in spite of the user pushing the turning buttons. This turned out to be caused by bending of the user’s own spaceship. At first I reduced bending for “user owned objects,” hoping that it wouldn’t diverge from the server position significantly. I couldn’t turn it off completely because over time the client and server positions did diverge. This is because the physics are not deterministic.

However, surprise!, the angle of the player-owned ship is deterministic, because it always turns by a fixed amount, either clockwise or counter-clockwise, and as a result, the orientation angle is a direct function of the number of game steps where the left arrow was pressed, minus the number of game steps where the right arrow was pressed.

So by turning off bending on the angle completely (on user owned objects only), but not on position bending, I got the smooth behavior I needed. Isn’t that great? I just saved you the trouble of reading this book:

No spoilers, please.

Challenge 2: Collision detection.

The Symptom: “I hit the spaceships with a missile but the spaceships don’t get killed”

At one point we realized that the client and the server disagreed about kills. A kill occurs when a ship collides with someone else’s missile. We knew this problem was coming, because we’d read about it in the literature. The effect we were seeing was “false positives” — i.e. the client predicted kills which the server thought were misses. This is a serious problem.

The solution was quite simple. I set a more permissive (larger) collision radius check on the server than on the client.

Here is a snippet of a trivial implementation for collision detection. A real game would use a more advanced algorithm like quad-trees or spatial hashing.

The slow, but very simple brute-force collision detection.

This eliminated the false positives, but introduced false negatives. Turns out this works really well. False positives do not impact the game experience like false negatives do. If you shoot at another spaceship and at first you think you missed by a little bit, but then that ship explodes after all, you’ll tap yourself on the shoulder, and easily convince yourself that it makes sense — after all you’re a great player ;) On the victim’s end, you saw the missile coming and you thought you just made it out on time but then suddenly you see this dramatic message:

In this article we covered only two challenges. There were plenty more - which I cover in Part 2 of this series. To read Part 2 follow this link.