← Building Empire Builder: all parts

Series · 3 parts · 18 min read

Building Empire Builder

How Empire Builder: Digital came to be.

Part 1 · · 5 min read

Getting Empire Builder off the ground

Every year we came home from GenCon with a list. Games we'd played or watched over the weekend and promised ourselves we'd make time for. Empire Builder was always on it.

And every year, the list would slide right on by. Work, family, life. The list always loses.

"Surely there's a digital version"

In late 2022 we had what felt like an obvious thought: we play video games all the time, so why couldn't this be one of them? Surely someone had already made a digital Empire Builder.

A quick search turned up nothing. I shrugged and wrote the idea off.

My dev partner, Derek, did not. He went looking for whoever could make that game happen, half expecting to learn it was already in the works, and half wondering whether it was something we could do ourselves. There's a longer story there, but the short version is that he found Darwin's Toys and worked out an agreement that everyone involved felt good about.

Just like that, a couple of guys who couldn't find a free weekend to play a board game had signed up to build one.

A team of two

Derek and I were, and still are, the only developers and artists at Zombie Possum Games. I don't see that changing anytime soon. Then again, I'm sure plenty of successful studios started as one or two people who were certain they'd never need to grow.

Building a digital version of a game we already loved seemed like a gentle way to step into game development. An established game comes with a lot of built-in advantages, and the biggest one, we figured, was that we wouldn't have to design the rules.

They were already written down for us!

The rules are already written (aren't they?)

They were. Across multiple editions, released over decades, with contradictions and wildly varying levels of readability.

If I'm being honest, it was kind of a nightmare.

Empire Builder is played competitively at conventions around the world. There's an entire organization, the Train Gamers Association (TGA), built around it and games like it. The rulebook we worked from was 8 pages long. The errata document that came with it was 18 pages.

So "the rules" turned out to be a conversation between three groups: us, Darwin's Toys and the TGA. Add a healthy dose of hubris, the misplaced confidence of two developers who were sure they knew how the game worked, and you get a lot of collisions between what we built and what the rules actually said once the errata weighed in.

There was a lot of rework.

Starting before the ink was dry

I started building systems before the contract was even signed. I didn't need every hard-and-fast rule nailed down to know what the map looked like or how trains moved across it, so I dug in.

That's where the next hurdle showed up: the engine.

We chose Unity mostly because it was the only modern engine either of us had used. I'd been dabbling with it for over a decade. Derek had never touched it, but he's an accomplished C# developer, and by the time we got serious, Unity had good support for modern C#.

Knowing C# and knowing Unity turned out to be two different things. Derek came from years of building software for larger companies, and I think he'd admit that some of those habits and patterns don't translate directly to Unity, or to games in general. Today he's every bit as strong a Unity developer as I am, at least in everything this game touches. That didn't happen overnight.

The hard part, for me, was not assuming every disagreement meant he just didn't get it yet. There were plenty of grumpy late-night conversations that bled into grumpy early-morning ones.

These days we're more like a cantankerous old married couple. We mostly argue about money. That, and he refuses to put the toilet seat down.

Data first, because one map was never the plan

One principle we adopted very early: drive as much of the gameplay and map information through data as possible.

That came straight from our ambitions. We never wanted to make just the Europe map. We wanted (and still want!) to release all the major Empire Builder maps, and the only realistic way for two people to do that is to keep each map's specifics out of the code.

The catch is that the moment you have data, you need tools to manage it. From day one we were producing map data and map-specific artwork, and eventually sound, all of which needed tooling to maintain.

I'd argue our tooling could be better than it is. But in any project like this, internal tools are the first thing to slide to the back burner when there's a player-facing feature waiting to be built.

What came next

That's the beginning: a list we never got through, a search that came up empty, a partner who wouldn't let it go, and two developers who thought the hard part (the rules) was already done.

It wasn't. And the hardest problems were still ahead of us: building multiplayer for a game no off-the-shelf networking solution fit, watching our engine's licensing go sideways in the middle of development, and learning what it really means to build a game alongside another company. More on those soon.

Part 2 · · 6 min read

Multiplayer for a board game

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 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 web project was garbage. But, my second was better, and so on. In the 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.

Part 3 · · 7 min read

Two years to find a path

I'd been playing around in Unity for about a decade before we started on Empire Builder. Game development always felt like the ultimate creative outlet for someone who also happens to write code.

Back when Windows Phone was a thing, I even released a holiday-themed endless runner. You played a snowman racing through a frozen world, bouncing from ice block to ice block and dodging spikes and campfires. To this day, it's unclear where all the negligent campers were.

A tool already in the bag

RimWorld has been a huge inspiration for me. It's incredibly complex under the surface, but it has a visual simplicity that makes game development feel approachable. Top-down, simple artwork, a world full of little characters finding their way around it. I started (and abandoned) a lot of prototypes loosely based on that look.

I don't know that RimWorld uses A* for its pathfinding, but there's a good chance. Either way, those prototypes meant I'd implemented simple versions of A* several times. It was already a tool in my bag.

If you haven't run into it: A* finds the cheapest route between two points on a graph. It explores outward from the start, but instead of searching blindly in every direction, it always looks next at the option with the lowest combination of "what it cost to get here" and "a guess at what it'll cost to reach the goal." With a sensible guess, it finds the cheapest path without exploring most of the map.

And an Empire Builder map is a graph. Every milepost is a node, and every legal step between neighboring mileposts is an edge. So A* was the obvious choice.

A late-game map of Europe in Empire Builder, covered in mileposts and colored track built by several players
Every dot and triangle on the map is a milepost: a node in the graph the pathfinder searches.

What I'd never done was build a feature-rich pathfinding system: one with real costs and weights, special cases, and rules that change depending on what you're trying to do. It turns out that was the hard part.

Two jobs, two kinds of hard

We use pathfinding for two very different things.

Building: what will this cost me?

When you build track in Empire Builder, you're spending money, and the price depends on where you build. Open terrain is cheap. Mountains cost more. Alpine mileposts cost more still. Building into cities has its own costs, and so does crossing a river or other waterway.

So the building pathfinder's job is to find the route that costs the least money. Not the shortest route; the cheapest one. This is by far the most complex part of the system.

At release, it worked... okay. But it had enough issues that players who trusted it, without double-checking that it had actually found the cheapest route, were putting themselves at a disadvantage. In a game about money, that's a real problem.

My favorite example: more than six months after release, we found that the pathfinder wasn't properly accounting for crossing waterways. It would happily suggest a route that zig-zagged across the same river several times, at $2 million per crossing.

It was a bad route that looked perfectly reasonable on screen until you added up the bill.

Track around Amsterdam, Antwerp and Essen, with the Rhine and Meuse winding between the mileposts and crossed by several players' routes
Around Amsterdam and Essen, the Rhine cuts across nearly every route. Each crossing adds to the cost of building.

It turned out to be two bugs stacked on top of each other.

The first: when the search decided which step to explore next, it was going by distance instead of by what the route would actually cost. Fixing that helped well beyond rivers. Building through the Alps, for example, where there are lots of expensive mileposts packed together, got noticeably better.

But even with that fix in place, rivers still weren't being handled properly. When I dug in, I found that the code pricing each step was calculating the cost of crossing water. It just never added it to the total.

Both were head-smackingly obvious once I found them. But I didn't find them until I sat down and focused on that one problem and nothing else.

I put that down to moving at speed. When you're trying to sprint all the time, you're going to make some mistakes. Some of them will sit quietly in your code for months, costing your players $2 million at a time.

Today, the building pathfinder produces a predictably efficient path.

Moving: what did you mean to do?

Moving trains is simpler math, but it's complex in a different way.

Moving along your own track is free. But Empire Builder also lets you pay to use another player's track for the turn. So when you move your mouse across the map, the pathfinder isn't just finding a route. It's guessing your intent. Did you mean to stay on your own line, or did you mean to pay to cut across someone else's?

Even at release, we didn't have this quite right, and players complained about accidentally using (and paying for) another player's track.

Today, a dedicated piece of code interprets what the player is trying to do. It really boils down to one question: does the player want the fastest path using only their own track, or the fastest path regardless of whose track it uses?

The last annoying case was a player hovering over a major city milepost they aren't directly connected to. Is that a destination they're trying to reach on their own line, or an invitation to rent someone else's track to get there because it's cheaper (in movement terms)?

With our most recent major release, I believe movement is finally in a good spot.

The better part of two years

If I'm honest, getting all of this right took nearly two years. Not two years of constant work, but it was almost two years before I felt there were no outstanding bugs and I could call it feature-complete.

That's a long time for an algorithm I already "knew." The lesson I took from it: knowing the algorithm is maybe 10% of the job. The rest is the rules of your game, every edge case they create, and how much your players can trust the answer you give them.

The guy who owns pathfinding

I wrote most, if not all, of the building and movement code. I started on it before we'd even signed our agreement with Darwin's Toys; I'm not sure Derek had even installed Unity yet. Ever since, it's simply been easier for me to take pathfinding issues and let Derek work on something he knows better.

That's how it goes across the codebase. I wrote the very first versions of the networking and lobby code, but Derek has been on point for both ever since. I own much of the core building and movement code, and I wrote nearly all of the code that supports our DLC maps.

This is a weakness of small teams. Domain silos aren't just inevitable; they're necessary.

We could probably move faster if both of us knew every line of code in the game. But the time and mental load it takes to build that knowledge, and keep it current, is very costly when there are only two of you. It's easier to define clear areas of ownership and cross those boundaries when we need to swarm a feature or a problem together.

But "necessary" doesn't mean permanent. The networking code started as mine and became Derek's. Ownership shifts when the work demands it. The silos are there to keep us fast, not to keep us apart.