How Design Ships

Contracts Between Game Systems

How to keep game systems easy to change and easy to combine, and keep the docs that describe them from going stale.

The more systemic a game is, the more its systems lean on each other. That interconnection is the whole appeal, the place the emergent moments players talk about come from. It is also what makes them hard to work on: a change in one system can ripple into others you weren't expecting, and a design doc can fall behind the code the same afternoon you wrote it. Once a doc stops matching the code, people stop trusting it.

I've run into this on every interconnected game I've built, on both sides of the line: the designer writing the spec, and the engineer writing the code. Over time I worked out a way to keep systems easy to change without making them dangerous to combine. What follows is that approach. Read it as a thesis, not a law. Here's the idea, where it comes from, and a worked example from a game I build in my own time.

Two forces that fight

The trouble comes from two things pulling against each other.

Systems change constantly, especially early, when you're tuning and rebuilding them almost daily. And systems depend on each other, so touching one risks breaking three. The more they lean on each other, the more every change spreads, until nobody wants to touch anything.

You want both at once: systems that are easy to change, and systems that are easy to combine. Left alone, those two goals eat each other.

The fix isn't mine

Software engineers solved this a long time ago, and the rule is short: hide what changes, publish what others can rely on.

David Parnas wrote it down in 1972 and called it information hiding. Bertrand Meyer built a version of it he called design by contract. The idea is that a piece of code makes a few clear promises and hides everything else, so the people using it depend on the promises and never on the guts. None of this was built for games. The only thing I did was carry it over to how I write design docs and draw the lines between systems.

Design the contract, not the implementation

That's the whole thing in one line.

A system should show a small, steady surface: what it takes in, what it gives back, and what it promises no matter what. Everything else stays hidden. Other systems build on that surface. Behind it, you can rewrite and retune as much as you want, and as long as the promises hold, nothing downstream breaks. That freedom is what lets one system move fast without dragging the rest of the game along with it.

Everything below is just this idea, applied.

What it looks like in practice

A few habits fall out of it.

Keep the durable parts and the tunable parts apart. Before you write a line in a contract, ask one question: if I changed a number or rewrote the code behind it, would this still be true? If the answer is no, it doesn't belong in the design doc. It belongs in code or tuning data. That single question is what keeps a doc from going stale.

Hand other systems verbs, not settings. "Apply this force" is a verb. "Set the drag coefficient" is a setting you let leak. The moment another system reads your tuning values, those values become part of your contract, and you've quietly glued back together the two systems you were trying to keep apart. Offer the verb, hide the setting.

Let each fact live in one place. Every seam has two sides, but a fact written in two docs will eventually disagree with itself, and that's stale docs in a sentence. The system that provides something owns how it works. The system that uses it just points to that. When the provider changes, the link breaks loudly, so you catch the problem instead of shipping it. This one habit did more for my own stale docs than anything else.

Promise as little as you can. Every guarantee you publish is something you can no longer change. "Forces always apply on the next step" is a law worth promising. "Braking takes half a second" is a number you'll want to tune, so leave it out. A good contract is careful about what it won't promise.

Two smaller things round it out. Write down what each system depends on, so the map of your game's seams is visible and a loop like A needs B needs A shows up as the obvious problem it is. And put the things everything shares, like a currency or an entity model, in a foundation that belongs to no single system.

What a contract looks like

Here's one of mine, cut way down. In a game I build in my own time, the movement system owns all motion. This is a heavily simplified slice of its real contract:

# Movement (Systems)

## Provides
### Inputs   (verbs other systems send in)
- AddImpulse(vector)       one instant push: recoil, a bump, a bounce
- ApplyForce(vector, src)  a steady push: gravity, a current, a tractor beam
### Outputs  (facts other systems read)
- velocity, speed
- speedFraction            current speed, 0 to 1

## Guarantees
- Movement is the only thing that writes velocity. Everyone else uses the inputs above.
- Every impulse and force gets applied, never silently dropped.
- Speed stays under the active cap. (The cap's value is tuning; that it's always enforced is the promise.)

## Consumes
- Player or AI control input (the direction to push)

## Tuning (private, not part of the contract)
- acceleration, top speed, drag        live in data, change anytime

The whole idea sits in that Guarantees block. Recoil from a gun, a gravity well, a bump from another ship: every one of them moves the object through AddImpulse or ApplyForce, never by touching velocity directly. Behind that promise I've rebuilt how movement actually works more than once, and nothing else had to change.

A health system in the same game makes the point harder. It has no "take damage" call at all. You hurt something by making something fast crash into it. With no direct input to reach for, no other system can quietly break the rule.

Where it costs you

This isn't free. Writing a contract is slower than jotting a number into a doc, and the cost lands before the payoff, so on a small prototype it might not be worth doing at all. Not every system needs one either. The ones sitting at busy seams do; a system nothing else touches can stay loose. And the hard part was never the idea, which fits in a sentence. It's holding the line every day under a deadline.

Where I land

I can only speak for my own work. On the systems I've owned, drawing a clear line around each one, and being honest about what was a real promise and what was just a number, is what let me stop being afraid to touch them. They got easier for me to change and to combine, and that was worth the cost it added up front.

I used this structure extensively on Arma Reforger, still the most systemic and emergent game I've ever worked on.

Plenty of good designers would make a different trade. Some would call this much structure overkill, and on the wrong project they'd be right. That's the spirit to read it in: not a rule anyone handed me, just what I found true doing the work.

So that's the thesis. If you run it on your own game, I'd like to hear where it held and where it broke.

← More writing