Game Dev · · 6 min read
Multiplayer for a board game
Why no off-the-shelf networking solution fit a digital board game, how we ended up rolling our own messaging on top of PlayFab, and why the messes were worth making.
Your first game should NOT be multiplayer.
This is a piece of advice I've seen written in a lot of places.
I can say with some authority that this is really great advice.
We didn't take it. Empire Builder is a game you play with friends. Up to six people around a table, building track across the same map and racing to the same goal. A single-player-only version was never really an option for our initial release, so multiplayer was a day-one problem.
Modern engines make networking look easy
In most modern engines there are native tools, or add-ons, that handle a lot of the boilerplate networking for you. Keeping objects in sync across game clients, updating positions in real time, that kind of thing, is a few button clicks away in Unity and Unreal.
Okay, I'm oversimplifying. But take it from me: getting two clients talking to each other is very easy in a modern game engine.
A board game isn't a platformer
The trouble is that those tools are built for a different kind of game. A digital board game doesn't have enemies charging at you or characters running and jumping. Streaming a constant flow of positions and rotations back and forth between clients doesn't make much sense when the game happens one turn, one decision, at a time.
What a digital board game does need is a whole different set of answers:
- Which client it the source of truth? Which player's copy of the game holds the "real" state, and does the host's machine do the processing?
- What happens when clients disagree? If two players' games end up seeing different things, who wins?
- What happens when someone drops? A player losing their connection halfway through a two-hour game can't end the game for everyone.
The list goes on. For a game like ours, there was no plug-and-play solution. So we rolled our own.
We didn't write our own network stack
Don't get me wrong. We didn't write a network stack from scratch. That's for crazy people, or at least a specific kind of crazy person. We're a different kind of crazy.
We chose PlayFab, and it had a couple of nice synergies for us:
- Direct support for Unity. That was nice.
- Native support for Steam. We knew Steam would be our first platform, so this was big.
- It's a Microsoft product, built on Azure. For some folks that's a non-starter, and that's fine. Derek and I built our careers on the Microsoft stack, so for us it felt like a win, and it opened up plenty of integration options down the road.
At the jump, PlayFab gave us a place to store player information and game (title) data, and to capture gameplay telemetry. We've have plans to leverage more PlayFab features, like game saves to persist games across multiple sessions.
A means, not a message
What PlayFab gave us was a means to communicate. It didn't tell us what to communicate, or how.
At that point, Derek was still figuring out how to install Unity, so that decision, right or wrong, fell to me. I started building a message-based communication model: every meaningful thing a player does in the game becomes a message that the other players' games receive and apply.
That model is still the foundation of what we have today. I was intent on building something that "just works," and I did. But along the way I made several messes that we had to clean up later.
Weight we didn't need to carry
Our biggest mess came from diving into implementation before we really understood how PlayFab worked.
PlayFab makes certain guarantees about how messages are delivered. Order of delivery, for example, is handled by the API and SDK, so as a developer you can depend on messages arriving in the order they were sent.
We didn't know that. So we built a lot of weight into our messaging model to handle problems the platform was already solving for us.
Gutting all that wasn't hard. But it was a meaningful amount of wasted work, on a two-person team where every hour counts.
Trust, but verify
The flip side surprised us just as much. Like so much vendor documentation, PlayFab's sometimes told us we didn't need to worry about things that, in the end, we definitely had to worry about.
So we were overbuilding in some places and underbuilding in others, and in both cases the documentation was only part of the story. The rest you learn by running the thing.
Where the abstraction runs out
This was also about the time we learned that the PlayFab SDK for Unity is an abstraction layer sitting between Unity and the real binaries underneath.
That's a big part of why PlayFab is so easy for new developers to adopt. It's also why you have a finite toolset to work with. Eventually we started bumping up against the guardrails, and the documentation on how to extend the SDK was murky, at least at the time.
Paying for help was worth it
On the plus side, we were paying PlayFab customers right out of the gate, and that came with a pretty satisfying support model. When we needed help, we typically got it in a timely manner.
For a tiny studio that doesn't have a networking specialist down the hall, that matters more than any feature on a comparison chart.
Do it, fail, redo it
Knowing what I know now, I'd absolutely approach it differently, mostly to avoid all those refactors. I'd spend a little more time up front learning what the platform already promises, and a little less time building around problems it had already solved.
But I only know what I know now because I did it, failed, and redid it. So if you're staring down a problem like this, don't let the fear of getting it wrong pull you into analysis paralysis. Build the thing. Clean it up later. You'll clean it up better than you would have planned it.
When I started my development career thirty years ago (Christ on a bike, I'm old!), my first website garbage. But, my second was better, and so on. In th same sense, my second game will be better than my first.
But don't miss the point, it will be better because of my first, not in spite of it.