Skip to content

How I built Greedwave, or: teaching zombies that a door is a price

I made a zombie game. It runs in a browser tab, no install, four of you barricade one building, a helicopter keeps landing, and every landing turns into an argument about whether to leave. It’s called Greedwave. It lives at greedwave.com, go die there.

This isn’t a post about whether the game is fun, you can decide that yourself in about six minutes. This is about how one person built it in Unity, with Claude Opus doing a good chunk of the typing, aiming at mobile and somehow shipping a browser game first. Also about how hard it is to tell a zombie “if the door is boarded, go around” without the zombie either giving up or ignoring the door entirely.

One guy, one model, one engine

No team. No artist on call, no second opinion at 2am. What I had was Unity, a mobile build target and Opus in the loop for pretty much everything: systems code, tooling, the boring glue, and a lot of “explain to me why this coroutine is on fire”.

I’ll be honest about how that works because people either oversell it or pretend they don’t do it. It does not design the game. Every mechanic below came out of playtesting and being annoyed. What it does is turn “I want barricades to cost the horde time instead of blocking it” into working code fast enough that I can find out the idea is wrong the same evening instead of next week. The speed of being wrong went way up. That’s the actual feature.

Mobile first, browser first

Phones were in the design from the first day, not as a port — that’s the expensive way round — but as a constraint on each UI and performance decision while it was being made. The build target, though, was Windows. And the thing you can actually play today is a browser tab.

The desktop package still builds and still runs. It just isn’t being published while the browser version is the one getting pushed. A link you can hand someone beats an installer they have to decide whether to trust, and the WebGL build came out as the whole game rather than a slice of it, so there was nothing left worth holding back for the desktop.

The WebGL build is the full game, not a demo of the demo. A free account keeps your progress on the server, so a raid you banked on the laptop is already there on the phone. No ads, no tracking, no third-party SDKs. The account exists to hold your coin and nothing else, because I don’t want to know anything about you and I definitely don’t want to store it.

What WebGL cost me was three small holes that had to be patched from outside the engine. Browser audio won’t start until the player has touched something, so there’s an unlock path and a jslib behind it. The engine also reports whether touch exists and what the DPI is, and in a tab it’s wrong about both, so those answers come from a second jslib that asks the browser directly. And the network client needs a WebGL path of its own, because in a tab you get a WebSocket and nothing else.

iOS is in review preparation, and what’s left there is account and store work rather than code. Android doesn’t have a build target yet.

The scarce resource is seconds, not boards

The classic move in zombie games is to make boards scarce. Three boards, five doors, tough luck. I went the other way: boards are unlimited and one board already closes a door. But boarding the whole house takes 14 seconds, the motel 28, and the horde is already walking in.

That turns the question from “can I close the doors” into “which door do I close”. One number, the boarding time, replaced an entire inventory system with a priority problem. A lot of code got deleted. I was very happy that day.

A barricade is not a wall, it’s a price

This is where the real work started. Make barricades impassable and the horde piles up at the door looking stupid. Make them ignorable and there’s no point boarding anything.

So a barricade is a cost. A door boarded to full costs the horde about eleven seconds of chewing. If a way around is shorter than that, it walks around. Either way the barricade did its job: the door you board is the door the horde stops using.

And the thing being weighed is damage, not bodies. One Brute counts like five Shamblers at a barricade. That one rule answers “why are they coming from there” without any hand-tuned spawn table. The horde just keeps doing the math.

The mechanism is a flow field, not a path per zombie. One multi-source Dijkstra over the grid, seeded from every living player’s cell, and what falls out is distance-to-nearest-player for all 1,872 cells of the map. Every zombie reads the field and walks downhill. The cost doesn’t care how many of them there are: 120 zombies read the same array.

The first version used BFS, and that’s the version where boarding a door did nothing at all. With equal-cost steps the horde always walks to the nearest door, and the door you just boarded is still the nearest one. Barricades only start to mean something once a step can be expensive: the boarded door stays walkable and simply costs more, and Dijkstra does the comparing. If there’s an open way in, the horde finds it. If you’ve closed everything, it leans on the weakest point.

The field rebuilds about six times a second, and the grid carries a version number so boarding a door invalidates it immediately. A binary heap over 1,872 cells isn’t a cost worth optimising, which is why there’s no bucket queue in there. Zombies move on transforms with a spatial hash for neighbour queries rather than Rigidbody2D — that one is a mobile decision, and it’s why 200 of them fit on a PC and 120 on a phone.

Two bits

Outside, a single “walkable” boolean wasn’t enough. There are two questions: does it stop a body, does it stop a bullet. Anything waist-high, the car in the driveway, the sandbags in the yard, stops a body but not a bullet. That’s a firing position: the horde has to come around, you don’t have to stop shooting. A crate or a hedge stops both.

Two bits. Every map is built on those two bits.

Twenty-six percent

Elites climb each phase and cap at 26 percent of the horde. Past that they stop being events and start being a queue at the tax office: everyone’s there, nobody’s special, all your ammo is gone.

The climb is 0, 0, 8, 17, 26. About nine points a phase, and 26 is where the climb ends rather than a number I sat and tuned. There used to be a sixth phase after it, and cutting it is what taught me the cap: it was the same question asked twice, the weights moved six points, nothing else changed, and a finale that changes nothing is a wait. Its load went into phase 4 and the ending.

Phase 1 is all Shamblers, that’s where you learn the building. Runners join in phase 2 and spraying the crowd stops working. Brutes hit in phase 3, the Bloater and the Armored come together in phase 4, and the Colossus closes the run.

The best loot shows up at the worst time

Weapons land on the floor as a coloured piece you have to walk to. The colour is the tier and every roll shifts upward each phase, so the good stuff drops during the minutes you least want to leave cover. Crates roll better than the floor, sealed crates best of all. Prying one open pulls seven zombies to exactly where you’re kneeling.

The better tier isn’t bought with coin. It’s bought with the horde learning where you are.

POCKET (UNSECURED)

Most honest line in the HUD. Coin you pick up sits in your pocket, not the bank. Die and it’s gone, extract and it’s yours. XP you keep either way.

On the backend that’s two ledgers, pocket and bank, and the client never gets to say “I banked this”. Only the helicopter says that. The server is Go, a WebSocket per player, SQLite for the profile. There’s no save file on the client at all, which is where cross-save came from for free: same account, same progress, nothing to export or import.

It’s on the server because it had to be. While coin, XP and inventory lived on the client, the whole progression was one text editor away from meaningless. Now the client sends intent — buy this — and draws whatever snapshot comes back. Inside a raid is a different story: the simulation is host-authoritative and the server never sees it, so the kills and coin a session reports at the end can’t be verified, and they get capped. XP the server works out itself. The price of all that is there’s no offline mode any more. No server, no game.

The helicopter lands four times a run. You don’t pick the pad and you don’t know which bird is coming: the unarmed transport or the gunship. The last window is always the gunship, and it’s also the last way out. Miss it and the raid stops being a fight. Siren, a bomber circling overhead, 45 seconds. You can still run, you can still shoot, and everything you were saving is going to burn.

That’s the name. Each wave you get a little greedier.

Four people, one building, one gun on the floor

Co-op up to four. Everyone boards the helicopter on their own; one player extracting neither splits nor deletes anyone else’s coin. But what’s on the floor is shared. A drop belongs to the room, not to whoever killed for it, and only one of you walks away with it. Deciding who is your problem, not the game’s. I didn’t solve that on purpose. The best fights come out of it.

Transport is a WebSocket per player and one of the four is the host: it runs the simulation, the others draw what it tells them. The server stays outside the raid and only arbitrates the economy.

Not built yet

It’s a demo and hiding the edges is pointless. The old signal design is retired. Still missing: a skill tree, banking a looted weapon at settlement, barricade repair, and the Android build.

Go play

greedwave.com. Open a tab, free account, done. If you die, don’t write to me. You should have got on the helicopter.

← All posts