Back to Blog
procedural generationlevel designsystems designprototyping

Procedural With Intent: What to Generate and What to Hand-Build

Most small-team procedural generation fails because the machine is asked to invent the wrong half of the level. A practical split between authored rooms and generated arrangement, with testable constraints, seed workflows and a pre-build checklist.

GameDesignerX TeamOctober 16, 20267 min read

Your generator produced a cave. It is technically a cave: it has walls, a floor, an exit somewhere to the east, and seventeen bats. A playtester walked through it in forty seconds, said "huh," and asked whether the level was finished. Nothing was broken. Nothing was interesting either.

That gap — between valid and worth playing — is where most small-team procedural generation dies. The fix is almost never a cleverer algorithm. It is deciding, explicitly, which parts of your level a machine is allowed to invent and which parts a human must author, then writing that boundary down before anyone opens a noise function.

Procedural generation is a content multiplier, not a content source

A generator does not create content; it recombines content you already made. A dungeon generator assembling 12 hand-built rooms produces layouts that feel like your 12 rooms — and if those rooms are dull, infinite variations of them are dull at scale.

This is why the games people cite as procedural successes are mostly hybrids. Spelunky builds each level from a 4x4 grid of room templates, with a guaranteed solvable path carved through them before decoration happens. Dead Cells assembles levels from hand-authored chunks per biome. Hades hand-builds every room and generates the sequence — which room, which encounter, which reward, in what order. In all three, a human made the moment-to-moment space and the machine made the itinerary.

So the first design question is not "what algorithm?" It is: what is the unit of authored content, and what is the unit of generated arrangement?

Common splits that work for small teams:

Authored unit Generated layer Fits
Rooms with fixed entrances/exits Room graph, doors, locks Roguelites, dungeon crawlers
16x16 terrain chunks Chunk placement, biome blending Survival, exploration
Encounter presets (enemy + spawn pattern) Which preset, where, when Action, shooters
Set-piece levels Order, modifiers, difficulty scalars Mission-based games
Item stat blocks and affix pools Affix rolls and tiering Loot games

Pick one row. Teams that try to generate both the space and the content inside it usually ship neither well.

Write the constraints before the algorithm

A generator without constraints is a random noise machine with extra steps. Before you write code, write the rules the output must satisfy. Keep them testable — each one should be checkable by a function returning true or false.

A workable constraint set for a dungeon floor might read:

  • Exactly one entrance and one exit, and the exit is reachable from the entrance without destructible terrain
  • Critical path length between 6 and 11 rooms
  • At least 2 optional branch rooms, at most 5
  • No more than 2 combat rooms in a row
  • Every locked door has its key on the critical path before the door
  • At least one room with no enemies, placed in the final third (a breathing room)
  • Total enemy threat budget within ±15% of the floor's target value

Those numbers are starting guesses; you will change all of them after the first playtest. The point is that they exist as numbers rather than vibes, so "this floor felt wrong" becomes a measurable question: which constraint was violated, or which constraint is missing?

This is exactly the kind of thing that rots when it lives in one programmer's head. Constraints belong next to the level they govern — in GameDesignerX, the Levels module is a reasonable home for the per-floor targets (path length, room counts, threat budget), with the shared rules in Mechanics so designer and programmer are reading the same list.

Generate, then validate, then reject

The simplest reliable architecture for a small team is generate-and-check:

  1. Generate a candidate layout from a seed
  2. Run every constraint as an assertion
  3. On failure, log which constraint failed, re-roll with the next seed
  4. After N failures (say 50), fall back to a hand-authored layout and warn loudly

Step 4 matters more than it looks: a hard-coded fallback floor turns a constraint bug into a slightly repetitive level instead of a soft-locked build at a publisher demo. Step 3's logging is your debugging tool — if "key before door" fails on most candidates, your room templates likely lack connection points.

Reject-and-reroll feels wasteful. It is not: spending 30 candidates to get one good floor is far cheaper in developer hours than writing a generator that is correct by construction.

Readable randomness: let players learn the system

Players do not enjoy randomness; they enjoy reading randomness. A good generated space is one where the player can form a plan from partial information.

Techniques that make generated content legible:

  • Visual grammar for room roles. Treasure rooms always have the same door frame. Elite encounters always announce themselves with the same floor marking. The layout is a surprise; the vocabulary is not.
  • Fixed anchors. The shop is always on floor 2. The boss is always the last room. Variety lives between landmarks, not in place of them.
  • Telegraphed branches. Choosing between two unexplored doors needs one bit of information about each — an icon, a sound, a glimpse. A blind coin flip is not a decision.
  • Bounded extremes. Cap how bad a run can get. Bad-luck protection matters on layouts as much as on drop tables: if your generator can produce a floor with four elites and no healing, it will — on someone's first session.

Seeds are a production tool, not just a feature

Make every generated space reproducible from a seed, and surface that seed in the pause menu and in crash logs from day one. The payoff is in bug reports: "the stairs spawned inside a wall" is unfixable, while "seed 48812, floor 3" is a ten-minute fix. Pair the seed with the build number so a report from an old build is still reproducible — your build tracking and playtest notes should both carry it, which is why Build Tracking and Playtesting records are worth linking by seed in your own docs.

A tuning loop that converges

Generators resist the usual tuning loop because you are not tuning one level, you are tuning a distribution. Three habits help:

  • Batch-generate and inspect. Produce 200 floors headlessly and chart the distributions: critical path length, room-type mix, threat budget, item count. Outliers are your real bugs, and a long tail at 18 rooms tells you more than ten playthroughs.
  • Separate difficulty from layout. Keep enemy composition and threat budget in a data table, not in the generator. Then you can rebalance without touching layout code — and your balance sheet stays the single source of truth for numbers.
  • Favourite seeds as test cases. Keep a short list of named seeds: one typical floor, one worst case, one that previously broke. Re-run them after every generator change. This is regression testing for level design, and it costs almost nothing.

A pre-build checklist

Before you write the generator, answer these in writing:

  • What is the authored unit, and how many do we need for launch? (Count them. A roguelite with 15 rooms per biome will be exhausted in an afternoon.)
  • What is the generated layer — arrangement, selection, or parameters?
  • What 5–10 constraints must every output satisfy?
  • What is the fallback when generation fails?
  • How does a player learn to read the generated space?
  • Where is the seed shown, and is it in crash logs and playtest notes?
  • Which numbers live in data tables rather than code?

The scope question nobody asks early enough

The last question is the one that should come first: does this game need generation at all?

Generation earns its cost when the game depends on repeated play of unfamiliar spaces — roguelites, survival, extraction, endless modes. It is a poor trade in a 6-hour narrative adventure, where you spend months building a system whose output is worse than the 25 rooms you would have hand-built in the same time. "We'll generate the filler levels" is usually a sign the filler levels should be cut instead.

If the answer is yes, treat the generator as a design document that happens to execute. Its constraints are your pacing rules, its room set is your vocabulary, its seeds are your bug reports. Write those down, and the cave stops being technically a cave.