Airships uses a lockstep multiplayer system. You make sure that each player has the same starting state, and the same seed for the random generator. Then you make sure to apply player commands at the same time and in the same order for all players. This way, the game state on each player's machine progresses in the same way, in "lockstep".
Lockstep has the advantage that it's fairly simple and that it requires little bandwidth. The disadvantages are high latency (which for a strategy game we don't really care about) and a kind of "butterfly effect" thing. If there's ever the slightest discrepancy between the states on different players' machines, the states will diverge more and more over time, and cannot be easily reconciliated. So it's quite nice and simple if you get it perfectly right.
The combat mode got multiplayer early on and so has grown up along with it. There have still been plenty of problems, but it generally works now and doesn't diverge. The strategic conquest mode, on the other hand, is almost as old, and written on the assumption that it's singleplayer only.
Still, technically, all it has to do to be lockstep multiplayer is two things:
- Be predictable. Use the right random source across the board, and don't do stuff that relies on details of the player's computer, like the frame rate or details of memory allocation.
- Instead of directly manipulating the world, send command objects to the server, receive them back, and execute them at the right time.
The bigger problem is that I baked a whole bunch of assumptions into the code, such as:
- There is exactly one human player.
- That player is always the first empire on the map.
- Fights between all other empires can be quick-resolved.
- The game can be paused whenever it needs user input.
- Battles where commands are received through the multiplayer connection have exactly two human participants.
- Battles where commands are received through the multiplayer connection are not part of a bigger process. When they end, the "game" ends.
Well, you might say that making these assumptions was a bad thing for me to do, but it actually wasn't. Making the assumptions meant I could write simpler code and be more productive. Writing everything in the most abstract, generic, flexible way is a bad idea. You almost never need the abstractions, and when you do, you tend to find out that they are the wrong ones.
In these three days so far, I have set things up to use the right random sources, rewritten a bunch of code in strategic mode that assumed there was one human player, set up some code to go into multiplayer battles and back into strategic mode, moved all instances of the player directly manipulating the game state into command objects, set up code for sending and receiving commands, and created a basic lobby screen for MP conquest.
Two views of the world map, in sync.
And it... actually kinda works! You can set up a game between two players, and the generated world is the same. You can send a fleet to another city and it moves in sync. The fights appear to stay in sync.
Still, there probably are divergence issues, and there's a lot of things incidental to multiplayer that need to be made to work. Saving and loading, desync detection, dealing with disconnects. And the user experience is pretty rough right now: when a fight starts, players are simply yanked out of the map screen, straight into the fight.
The player on the right is fighting giant spiders while the one on the left is spectating.
So this is an encouraging stage to be at, but I always knew that the hard part was going to be getting all the details right. Onwards!