← Building Empire Builder: all parts

Series · 5 parts · 28 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.

Part 4 · · 5 min read

When your engine flips the table

We had the misfortune of being deep into development of our first game when Unity's leadership decided it would be a great idea to flip the table on their licenses and, by extension, on everyone who licensed their engine.

That was September 2023. Unity announced its Runtime Fee: a charge per install, on top of the subscription, once a game crossed certain revenue and install thresholds. We were roughly halfway to launch.

Two developers, two reactions

We weren't in a position to be hit by the changes right away. But it was a gut-wrenching moment, and it caused some real conflict between Derek and me.

We had different perspectives on how bad it was, what it meant for us as we kept building, and whether it was a problem we were ever likely to run into at all.

I was angry. For years I had trusted Unity to be the platform where independent developers could not just exist, but thrive. The new model felt like it was going to crush small developers.

There are a lot of people out there making games entirely for the love of it. My real fear was for the free game that takes off. Remember that Minecraft started as a free download on Notch's website. Charge per install, and a game that makes a little money while millions of people play it for free could be crushed by its own success.

Running the numbers on success

We also had to think about the other end of the spectrum.

We had, and still have, ambitions of selling so many copies that we'd have to worry about this kind of thing. Those ambitions are currently unrealized. But the question was real: could we survive being charged per install? Was there a tipping point where the arithmetic turned against us, where selling more meant earning less?

When a player becomes a cost

That question pointed at something we'd been thinking about since the beginning: keeping our cost per player low.

Empire Builder doesn't have in-game purchases. We've talked about cosmetics and the like, but today none of that exists. A player pays once, or a few times if you count DLC maps.

So what happens with the player who loves the game and plays it every day? Every service that charges us while they play keeps charging us long after their last purchase. Somewhere there's a threshold where a devoted player stops being a profit center and becomes a cost.

That's not a hypothetical. It's real and it has to be considered.

Too far in to turn back

We had been weighing all of these decisions throughout the project. The Unity announcement just forced them into the open, all at once, halfway to launch.

In the end, we felt we were too far in to turn back. We held out hope that the community outcry would push Unity back from the brink.

So we kept marching toward release, with our fingers and toes crossed.

Mostly in our heads

As it turned out, most of it was in our heads. Unity walked back the most offensive parts of the change within weeks, and eventually dropped the Runtime Fee altogether. For us, in the end, it was kind of a nothing burger.

It didn't feel like one at the time. A decision we'd made months earlier, choosing an engine, suddenly felt like it could sink the whole project.

Hitching your pony to the right wagon

We did learn a valuable lesson. Especially in today's world of SaaS products and third-party services, it's entirely possible to hitch your pony to the wrong wagon.

If you're a new independent developer, be careful about which dependencies you build into your product. Understand what they cost today, and what they'll cost later, when your fledgling studio trips some revenue limit that changes the arithmetic.

Derek and I were wary of this before Unity's announcement, and we've been very wary of it since. The situation we work hardest to avoid is one where we're spending money to support an existing player base in a game whose revenue has slipped below break-even.

Here's what that looks like in practice. Today, Empire Builder is played entirely between players' machines. One player's game acts as the host, and we've taken reasonable steps to keep everyone in sync and to prevent cheating. (We've never caught anyone cheating, but reasonable steps.)

A small but vocal group on the Steam forums would love for us to add asynchronous play. Think back to the days of yore, when people played strategy games by emailing their turns back and forth. In today's world, that means a service in the middle: something that keeps track of every game in progress, takes in each player's turn, and hands it to the next player whenever they get around to loading up the game.

That service is a cost that never ends, paid on behalf of players who paid us once. A service like that might seem like a drop in the bucket, but our bucket is pretty freaking small. So we're unlikely to ever build it.

It's not fun to tell your players no. Sometimes it's necessary.

Choose your engine, your backend and your storefront the way you'd choose a business partner. Whether you signed anything or not, that's what they are.

Part 5 · · 5 min read

Choose your partners with care

Last time I wrote about the partners you never sign anything with: your engine, your backend, your storefront. This time it's the kind you do.

Let me say straight away that our partnership with Darwin's Toys is fantastic. We love those people, and we love that working together has led to the games we've been able to release.

But I can't imagine how frustrating this process would be if we didn't have that great partnership.

Not everything is ours to decide

When you build a digital version of someone else's game, a lot of the big calls aren't yours.

The final word on the rules sits with another company. Most of the art comes from outside our two-person studio. And the release timeline is shaped, at least in part, by somebody else's calendar.

None of that is a complaint. It's the deal you make when you build on an established game, and it's a big part of why we could build this game at all. But it would be absolutely maddening if we didn't have such a close relationship on the other side.

The standstill

Here's the part nobody warns you about.

You sit down at the keyboard knowing that your next release, or even your next unit of work, depends on an answer, an email, or a set of files that another person at another company is working on. On their schedule, with their own priorities.

That's enough to make what is supposed to be a very fun endeavor feel a little too much like work.

With every map we've released, or started working on, there has been some point where we were effectively at a standstill, waiting on something from Darwin's Toys. Not because anyone dropped the ball. Empire Builder is just one of many irons Darwin's Toys has in the fire, and every one of them needs their attention. That's what happens when two small teams with different priorities are trying to move in step.

Some of this is just me. I hate dependency. I hate waiting on someone else to do something I could probably just do myself, but I don't want to duplicate the effort, so I wait. If I'm honest, I tend to take it as an affront that someone has let themselves become a blocker. That's not always a fair perspective. It's a pet peeve, and it really grinds my gears.

Waiting on a business partner is like waiting for the bus. You have no idea when it's coming, but you know it's already late. You really only have two options: wait, or give up and move on.

For a long time, I filled the gaps with tech debt. As I've mentioned before, our tooling never gets enough attention, so there's no shortage of work there. But it's not glamorous work, and doing it while you wait still feels a little like punishment.

These days I handle it better. I make sure I always have some side effort underway that I can shift my mind to, so a delay doesn't mean a full stop. That works most of the time. When we're under a deadline, real or imagined, I still get annoyed. But it's a lot better than where I started.

The partner down the hall

Not every dependency is on another company. The one that frustrates me most is the one I share a studio with.

Derek, like me, is a busy guy with a real life and a day job, and on top of that he owns or runs several other businesses. So sometimes he misses a commitment, and when my work depends on his, I'm the one who's stuck.

Organization and communication aren't his strongest suits. When he's running behind, I usually find out after I've already been waiting for hours. Being late, whether to arrive or to deliver, and leaving the person waiting on you in the dark? "Pet peeve" doesn't cover it. Maddening is more like it.

We do have a shared board to track our work. Derek updates it in fits and spurts. Coming up on a release, he's great at it, because at that point it solves a problem for him: it shows what's done, what needs testing, and what's left before we can ship. Then it goes quiet until I complain or the next release comes around. I'm better about keeping it current, but even I could do better.

Honestly, our partnership works in spite of all that, because of our history. Derek and I have been friends for more than 20 years and have worked together in some capacity for most of that time. We know what we're getting. I'm getting everything I just described. He's getting a cranky, short-tempered hermit who wants to move to the mountains and forget humanity exists.

It's not perfect. But it works.

What makes it work

With Darwin's Toys, it comes down to patience and good will. We genuinely like the people on the other side. So when they're running behind, it isn't hard to assume good intent and be patient, especially now that I'm not so single-threaded that a delay stops everything.

Some things are simple by design. Darwin's Toys is the arbiter of rule questions, so when one comes up, we ask and they tell us. There's rarely much more discussion than that.

It's a business now

Partnerships like this are the foundation of business. And if you ever want to release a game, at some point you're going to have to start thinking of it as a business.

That was a shift for us. We started this because we wanted to play a board game we never had time for. Somewhere along the way it turned into contracts, schedules, deliverables, and relationships we have a real responsibility to.

So choose your partners with care. And be sure to be as good a partner as you're hoping for from the other side.