An eight-slot multiplayer lobby with player names, faction crests and team groupings, over a map preview

How multiplayer works in a browser RTS

Game Design9 min readUpdated
ClaudeBuilt the thing
Adam SturrockDecided what mattered

Written up later from the commit history. Dated to when the work landed.

I want to play this against my friends, in a browser, with nothing to install.

That is the whole brief, and it sits in the game's elevator pitch next to the nine factions and the naval warfare. What you get today is a lobby with a room code and a public game browser, up to eight seats with humans and computer opponents freely mixed, free-for-all or teams, on any map the skirmish menu offers. Drag one player's row onto another to swap their colours and their starting corner. If a player drops out, their faction is taken over by the computer or handed to a teammate and the match keeps going.

Getting there meant answering one question about what travels over the network, and the answer shapes everything else in this post.

Two armies facing each other across a misty river at dawn, goblin wolf riders on the far bank and a dwarf shield line on the near one, a broken stone bridge between them

Only what you clicked crosses the wire

An eight-player battle with several hundred units on the field cannot send the world. Twenty times a second, transmitting every unit's position, health, order and animation to seven other machines is far more than a home connection has, and it gets worse exactly when the fight gets good.

So the game does not send the world. It sends what you clicked, and every machine works out the rest for itself. That approach is called lockstep, and it only works if all eight machines run the identical match from the identical orders.

There are 81 different orders a player can give: move, attack, build, train, cast a spell, set a rally point, load a transport, take a tunnel, and so on. Every one of them travels as an instruction rather than as a result. The message is never "that unit is now dead", it is "attack that", and all eight machines kill it at the same moment on their own.

Orders are gathered into turns, and a turn is a tenth of a second. The match only moves on when every player has submitted their orders for that turn, and your own orders are scheduled two turns into the future, which is the fifth of a second of slack that lets a command reach seven other machines before the moment it belongs to arrives. That delay adapts between one and ten turns depending on how good the connection is.

Bandwidth therefore depends on how fast you issue orders, not on how many units exist. A hundred selected units given one attack-move order, meaning go there and fight whatever you meet, is a single order of exactly the same size whether the map holds fifty units or fifteen hundred. That is why 9 factions, eight seats and a late-game army all fit down a connection that could never carry the world.

A large battle at sunset: dwarf shield lines meeting goblins and warg riders across a smoke-covered field, a burning fortress on the ridge behind

A desync is not a graphical glitch, it is two different games

Because the world is never sent, it can never be corrected. If two machines ever compute a different answer, there is no authority to say which one was right, and nothing arrives later to pull them back together. That is a desync, and it is the only genuinely frightening failure in this style of multiplayer.

Picture it once and you never forget it. A unit finishes a fight on 3 health on your machine and 2 on mine. It dies to the next arrow on mine and survives on yours. So it kills a worker on yours and does not on mine. Within seconds the two machines are simulating different battles, and both are drawing a confident, smooth, completely disagreeing picture of one.

That is why running identically everywhere is a hard requirement rather than a quality bar. It also means the failure has to be caught on purpose. Every ten turns, roughly once a second, each machine boils its entire world down to a single number. That number covers every player's resources and upgrades, then every unit in a fixed order with its position, health, what it is doing and what it is doing it to, plus building progress, production queues, rally points, projectiles and population. Then the machines swap numbers and compare.

The detail that matters at eight players is that the numbers are tracked per opponent rather than pooled together. One machine disagreeing with the other six means that machine has forked. Everybody disagreeing with you means you have. A two-player game never needed that distinction, because there was only ever one other machine to blame. When it happens, the match stops with a Match Desynced message rather than quietly diverging, which is the correct response when the only remaining options are stop or lie.

Proving the property is a separate exercise from detecting a breach of it, and that is proving the game runs identically every time. It compares worlds using the same calculation the live game uses, so the test cannot pass by testing something easier.

What has to run identically, and how to check yours

Lockstep is one trade with one clause. Bandwidth stops scaling with how much exists and starts scaling with how fast people click, and the price is a guarantee that identical orders produce an identical world on every machine, on every processor, on every run. There is no partial credit. A game that runs identically 99.9% of the time is a game that desyncs.

That makes the choice of networking model a question about your game rather than about your network. Lockstep is right when unit counts are large and player counts are small, and it is wrong the moment something inside the loop is outside your control. The usual offenders:

  • Order of iteration. Anything that walks a collection and acts on whatever it finds first. Sort by something stable before acting, not after.
  • Any randomness that is not your own seeded generator. One ordinary random number in a spell, one shop rolling its own stock, and the worlds part.
  • Maths that differs between processors, which in practice means the trigonometric and exponential functions.
  • The clock, the frame rate, local settings, anything the graphics know. A game that can see its own frame rate runs differently on a faster machine.
  • Anything whose answer is not a function of its input, which is where a language model lands, and which is why the model-driven commanders are barred from a network match entirely.

The portable half is the checking rather than the list, because the list is never complete. Running identically is not a property you assert in a design document, it is one you measure while people are playing: reduce the parts of the world a player can act on to a single number, exchange that number on a fixed cadence, and stop the match when the numbers disagree. One comparison a second buys you the exact second the split happened, which is the difference between a bug report and a mystery.

The seat that was not free: 619 monsters sitting in the third chair

Going from a hardcoded one-against-one to eight seats surfaced a problem I had never needed to think about. For most of the project there were only ever two players, so everything else on the map, the wandering monsters and the camps guarding a gold mine, was quietly filed under the third player.

Eight is now the cap, and the monsters moved to a slot of their own so that the first eight belong to real people. The catch was that maps saved before the change still said otherwise. Across the shipped classic map library, 93 of 230 maps between them held 619 monsters whose owner would now land on a real player's slot. In a three-player game that means a player starts the match already owning a camp of the creatures that are supposed to be hunting them.

The fix was to read the old maps differently rather than to rewrite 230 files. Maps carry a version, and an editor that could only ever place two starting positions cannot have meant a third player, so in an old map that owner is unambiguously a monster. New maps pass through untouched and keep their real third player. The wider work that lifted the game to eight seats, including the arithmetic that kept it running identically everywhere, is going from two players to eight.

A second one came out of the same habit of re-reading old code against a bigger roster. The loading screen used to release everybody as soon as the first other player reported in. In a two-player game the first other player is also the only other player, so it was correct by accident. At six seats it would have started the match while four people were still loading. It now waits for every human seat, names who it is waiting on, and offers a way out after thirty seconds.

A dwarf ironclad firing broadside into a burning goblin barge in heavy rain, wreckage and swimming goblins in the foreground

Why the language-model commanders stay out of a network match

The game can be played against a language model running a faction, described in letting a language model command an army. That option is deliberately unavailable in multiplayer, and the reason is the cleanest illustration of what lockstep actually demands.

Lockstep does not send the computer opponent's decisions any more than it sends yours. Every machine re-runs every opponent's thinking for itself and relies on them all reaching the same conclusion. A rule-based opponent does, because it is a fixed set of rules reading a world that is already identical everywhere. A language model does not. The same question returns different text on different machines, on different days, sometimes on consecutive attempts, and the first different order forks the match.

The block lives inside the match rather than in the lobby screen: a model-controlled seat is converted to the Hard rule-based opponent before the game starts, no matter what the lobby said. A modified copy of the game can claim anything it likes in a lobby, so the lobby is not the place to enforce a rule the match depends on.

Where to go and try it

The lobby is live. Make a room, send the code, pick any two of the factions and any of the maps in the map library, and the two machines will run the same battle from the same orders without either of them ever describing a single unit to the other.

The thing I want next is not more code, it is data from the wild: a desync report with a seat number attached, arriving from a real game on real connections with packet loss and a phone hotspot in the mix. The per-seat attribution exists precisely so that report points at one machine instead of at the match, and the first one that lands is worth more than another week of local testing.

Questions

Can I play against my friends?

Yes. There is a lobby with a room code and a public game browser, and a match seats up to eight players with computer opponents freely mixed in, in free-for-all or in teams. It runs in the browser with nothing to install, and a player who drops out is taken over by the computer or handed to a teammate so the game carries on.

What is lockstep networking?

Every machine runs the same match and only player orders travel over the network. Unit positions and health are never sent, because each machine works them out for itself by running identical rules on identical orders. Bandwidth then depends on how much you click rather than on how many units exist, which is what makes a huge battle affordable.

What causes a desync in an RTS?

Any difference at all between machines in what the game computes. Two machines walking a list in different orders, a maths routine that gives a slightly different answer on a different processor, or one machine rolling a random number from a different source. Once two machines disagree by one point of health they are playing separate games, and every later decision widens the gap.

Does the game use dedicated servers?

No. Players connect directly to each other, with the host passing traffic between the guests rather than every machine dialling every other one. The host only passes on the kinds of message it recognises, so nothing unexpected is ever amplified out to seven people. If the host drops, another seat takes over.

Can language-model commanders play in multiplayer?

No. Every machine re-runs every computer opponent's thinking for itself and relies on them all reaching the same conclusion, and a language model returns different text on different machines. Picking one in a multiplayer lobby gives you the rule-based Hard opponent instead, and that swap happens inside the match rather than in the lobby screen.

← All posts