I’ve made several toy games with friends, but whenever it came around to adding some simple multiplayer support, getting the networking running was always a royal pain. It seemed like each game had so many special cases for how game-state needed to be synchronized between players that it was impossible to decouple the netcode from the game logic. I wished for a network library that could handle all the state synchronization for me, but I could never find one.
The trouble is that most existing “game networking libraries” are actually mostly data transport protocols (built over UDP). These help ship the data to its destination more responsively than TCP and more reliably than UDP, but usually don’t help much with the coordination/synchronization of game-state between players. So I decided to make one myself! And I did! (It only took me 3 years; turns out it’s kinda tricky.)
I have a simple, working, and hopefully actually useful implementation at github.
- The networking code should need to know nothing about the game logic.
- Players should be able to join and leave at any point without disrupting the game. (This turns out to complicate things.)
- The game engine should be able to update the world between turns (in a deterministic and consistent way), as long as the worlds are all consistent at the next turn boundary.
I want to be able to use the networking code as a library without needing to intertwine netcode throughout my game logic to keep things working.
The main idea behind my approach is that, rather than keep track of all the state on the server1, have each participant run a copy of the “simulation” and have a server (or some handshaking) ensure that all the inputs to the “simultaneous simulation” are synchronized. The idea being that if each copy of the simulation starts in the same state and receives new events in the same order, then each copy of the simulation should stay consistent. The main caveat being that you cannot touch the game-state directly; you must use an event.
This has actually already been done several times before. In fact, I suspect nearly all RTSs use a variant of this approach. (Though I’ve been unable to find any open sources libraries. If you know of one please leave a link to it as a comment.)
The original Age of Empires was the first game (AFAIK) to make use of a “simultaneous simulation” approach. There so happens to be a great article describing their implementation on Gamasutra. They also have another great article on how X-Wing vs. TIE Fighter did something similar.
While the original purpose of the approach was to minimize bandwidth usage, it also conveniently minimizes the need for interactions between the game code and the network code.
The networking code needs to know nothing about the game-state. All it does is handle the handshaking and ensure that each participant receives events in the correct order. Actually, it needs to do one more thing: it needs to provide a concept of time. Sounds simple doesn’t it?
You just need:
- A way to de/serialize events. (Network code doesn’t care about what’s in them; it just needs to make sure they’re applied everywhere in the same order.)
- A special “turn” event (which provides our concept of “time”).
- A way to apply incoming events to the simulation.
If you want to support players joining/leaving mid-game, you’ll also need:
- A way to de/serialize the game-state.
- A special event for when a player is disconnected (since that player can’t send it themselves).
And that’s it! The network library doesn’t need to know anything about the internals of game.
Constraints this Places on Your Game
The main constraint is that you need to make sure nothing affects the simulation state except incoming events. This can be trickier than it sounds. How do you make sure you don’t have any unintended side effects? Probably the most common gotchas are time and random numbers. If you use a random number generator, you need to make sure the intermediate state gets stored in the game-state. Any times need to be derived from the “turn” events or sent into the simulation by some other event.
Enforcing these kinds of constraints are easy in a language like Haskell, but in a language like Ruby, you basically just have to be really careful. It’s actually pretty easy to do if you designed your game with this in mind from the start, but can be quite difficult to tack on after the fact. There is a another great article that describes many other benefits and potential pitfalls to controlling the game-state this way.
I think I’m going to take a break with networking for a while and work on some easier problems like logic solvers and program synthesis.
1 Why not use the client/server model? And just do all the important state tracking on the server? What I ran into when implementing a client/server model is that the client always needs to know about some state. You can’t just ship down all the state whenever something changes; so you have to pick and choose which state needs to be sent to clients and when, which is the same problem we started with.