← Devlog

I added three new enemies and drew none of them

Late-game waves had run out of scary. Instead of designing new villains, I cloned the ones I had into crowds — three new tanks for the price of one render pass. The catch: a crowd that looks like a crowd but acts like one guy is just a fat sprite.

June 23, 2026 devlogpipelinegameplay

Late-game waves had a vocabulary problem. I had lone enemies — a Kid, a Developer, a Worker, a Whale — and by the back half of the campaign I’d run out of ways to say scary. The obvious move was a new character, and a new character is expensive: an image gen, a 3D bake, a background cut, four walking directions baked off a render rig. Worse, every fresh generation drifts a little in style, so the new villain never quite matches the old roster standing next to it.

What I wanted was dumber than a new character. I wanted more of the ones I had. A crowd of kids. A horde of developers. A union you can’t rate-limit. The fiction was sitting right there.

The thing I already owned

My single-enemy pipeline is a tiny render rig: it loads a rigged 3D model, points an isometric camera at it, and bakes four facings — the same four directions every unit walks. The art that ships is just those bakes; the engine flips between them as the unit turns.

So the question answers itself. Why render one model? Render five copies in a formation and bake the cluster as if it were a single enemy. Same camera, same lighting, same four-direction output. The only new idea is what’s standing in front of the lens. That reuse is the whole point: I’m not building a crowd system, I’m changing one variable — count — in a pipeline that already works, so the crowd is automatically on-style and turns as it walks for free. I didn’t write a line of animation code.

Identical clones read as a bug, not a crowd

First bake — three kids — looked terrible. Not crowd terrible. Copy-paste error terrible. Three pixel-perfect children in a row, like a desktop icon that failed to deselect. The eye is brutally good at spotting exact repetition; the instant it clocks two identical things, the illusion is dead and you’re looking at a glitch.

The fix is jitter: shove each clone off true by a hair. Roughly ±8% on scale, ±13° on yaw. That’s enough that bodies read as different sizes facing slightly different ways — the way actual people stand when they’re collectively annoyed at you — and nowhere near enough to look broken.

The detail I’m quietly proud of: the jitter is deterministic, not random. Each clone’s offset is a hash of its position in the lineup, not a Math.random() call. Sounds pedantic, matters a lot — a random bake gives you a different crowd every render, so “tweak the lighting and re-bake” silently reshuffles every figure and you can’t tell what your change did. A hashed offset means same input, byte-identical crowd, forever. Reproducibility is a feature in an asset pipeline, not a nicety.

The other half is the feet. The figures sit at different depths — measured in footprint units, so front ones land lower on screen and back ones higher, feet on a common ground plane regardless of their jittered scale. That’s what gives the cluster real isometric depth instead of looking like a flat sticker someone slapped on the path.

One process note that paid for itself: I baked one crowd first and shipped nothing else until it read right in-game. That single POC surfaced the identical-clones problem while the fix was one line, before I’d committed to three units and a three-way redo. Prove the look on the cheapest possible case, then scale.

A sprite isn’t an enemy

Here’s where I almost fooled myself. The art was the easy half. A crowd that looks like a crowd but behaves like one guy is just a fat sprite — same HP-and-money math, scaled up. It doesn’t play differently, so it isn’t really a new enemy.

So crowds got their own balance verbs. First, leaking hurts more: a crowd that reaches the exit unserved costs you two or three lives, not one. A horde marching out the back should sting like a horde, and tying the penalty to the unit instead of hard-coding it means future crowds set their own price.

Second, and this is the one that actually mattered: one crowd per wave, max. My waves can roll a “single-type burst” — a run of the same enemy back to back. Without a cap, that burst could spawn four hordes at once: four tanks, simultaneously, with no reasonable answer. That’s not hard, it’s unfair — difficulty the player can’t read and respond to. The cap (extra crowd picks quietly fall back to the base unit) turns a crowd into punctuation: a rare heavy beat, not a wall. They also only appear in markets where their base enemy is already common and late, so they make thematic sense where they land.

I checked one level by hand and the per-wave crowd counts came out 0, 0, 0, 1, 1 — exactly the rhythm I wanted. They arrive rarely, and they arrive last.

What stuck

Three distinct crowd-tanks — a fast, tanky swarm of kids; a picky, heavy, jackpot horde of developers; and the Union, slow and enormous, whose whole personality is you can’t rate-limit a union — all four walking directions, fully balanced, and zero new art generations. They aren’t new drawings. They are the existing roster, standing closer together.

The lesson I keep relearning in a new costume: the cheapest new content is a rearrangement of content you already shipped. But “cheap art” and “free enemy” aren’t the same thing — the rig gave me the picture; the enemy only existed once it had its own leak cost and its own spawn rules. When the fiction lets you say “a crowd of X,” don’t draw a crowd. Crowd the X — then give it reasons to play unlike one.