Controlled Procedurality — How a Maze Generator Became a Level Design System

pavelzosim:~/atlas_SYS.ONLINE / UTC+3

01 Overview

I set out to build a maze generator in Houdini. It didn't stay one — and it was never really about finding the shortest path between two points anyway. A maze-solving algorithm does that in a page of code. What I ended up building is a modular system where abstractions describe rules for how a maze — or any level — gets assembled, and every stage of that assembly supports both randomization and manual placement.

Right now, every wall in the demo is a straight 90-degree segment. That's a deliberate constraint, not a corner I haven't gotten to yet. Angled corridors and non-orthogonal geometry mean more junction types to test, more assets to author for each of them, and a wall-building step that has to account for arbitrary angles instead of one fixed grid increment. Orthogonal walls keep the module count small and the placement math simple. Angled geometry is a real option later — once the rule system underneath is proven, not before.

Orthogonal-only walls are a deliberate scope constraint, not an unfinished corner. Angled/non-orthogonal geometry is deferred until the rule system underneath is proven — not implemented and hidden.

02 Borrowing structure from real maps

Take a real map. It's divided into grid squares, and each of those squares divides again into smaller ones. That's not a metaphor — it's literally how military grid reference systems work: a large square gets a code, and inside it, smaller squares narrow the precision down further, from ten kilometers to one meter, depending on how exact you need to be.

That's one layer: a grid for locating things. It doesn't say anything about what belongs in any given square.

The second layer is what a level designer adds on top of it. Half-Life 2 is a clean example — its levels aren't laid out as generic "here's an area." Valve's own designers have described the process directly: an initial idea that a stretch of level should contain a puzzle or a combat encounter, checked against what comes immediately before and after to keep the overall pacing right, then corrected against what playtesting actually shows.

"We might have an initial idea that we want a certain level to contain a puzzle or combat. We try to look at what comes before and after and keep in mind what we think the overall pacing should be. We learn everything from playtests." — Phil Co, Valve

Resident Evil runs a version of the same idea with different vocabulary. Nearly every game in the series, going back to 1996, uses save rooms — deliberately monster-free spaces that exist purely as a breather. Resident Evil Village layers a shop on top of that same pattern, through The Duke's Emporium — though that's specific to Village. The original RE7 has the save rooms but no merchant.

Neither game invented the grid. What both add is a layer of intent on top of spatial subdivision. The same square can be a shop, a fight, or a breather — and a level doesn't need to be built on literal squares at all for that idea to still apply.

TWO LAYERS OVER ONE GRID — LOCATION VS. INTENT
A grid used for location versus the same grid carrying design intent Left panel: a nested reference grid with no meaning attached, subdividing coarse squares into finer ones like a military grid reference system. Right panel: the identical grid, now with each cell labeled by design intent — shop, action, or breather. GRID — LOCATES SHOP ACTION BREATHER ACTION INTENT — DECIDES ROLE
Same subdivision, two jobsThe grid on the left only says where a square is, the same way an MGRS reference narrows a location without describing it. The grid on the right is identical, but each square now carries a design role — the layer Half-Life 2 and Resident Evil each add on top of their own spatial subdivision.

03 The cell as the unit of intent

A generated level running in the current game build, third-person view, final lighting
FIG 01 — A generated level from the current build. Debug view: none, final art and lighting. Note the module variety along one straight corridor stretch — same wall abstraction, different variants.

That's the layer I built into the generator. A cell is a slot that can carry a zone — shop, action, puzzle — independent of what fills it structurally, and independent of whatever prop rule applies inside it.

Cell — a slot that carries a zone rule (shop / action / puzzle), independent of the structural rule that fills it and independent of any prop rule applied inside it.

CellZone ruleProp ruleStructural rule
ASHOP — a lull zone, room to breathe and pick up ammo or health
BACTIONPUZZLE
CACTIONLAMPPOSTHISM wall

The lamppost in cell C isn't decoration. It's a landmark — the same way a real streetlamp standing alone in the middle of a real street quietly tells you this is a place, without a line of dialogue or a UI marker. Two cells can carry the identical zone label (B and C are both ACTION here) and still read completely differently, because the prop rule and the structural rule under that label aren't tied to it.

A shop-zone cell next to an action-zone cell, same underlying grid, different zone rules
FIG 02 — Same cell abstraction, two different zone rules, side by side. Note how the shop cell (left) and the action cell (right) read as distinct places without any change to the underlying grid.

04 Two categories: obstacles and actions

Everything the system places falls into one of two buckets.

Δ1 // Obstacles
Restrict or direct the player — a wall segment, a landmark like the lamppost above. Passive, spatial.
Δ2 // Actions
Behavior, resolved through Blueprint logic inside a given or randomly generated cell — spawning an enemy with its own initiation rules (proximity to other enemies, type, frequency), dropping loot, placing a chest.

Same underlying registration mechanism, different job.

05 A guarantee, not a coincidence

Doors and keys are one instance of this pattern. Each door carries an index, and the system guarantees a matching key exists somewhere the player can actually reach before that door has to be crossed. That's not validated after the fact and patched to fit — the guarantee is built into how the layout gets generated in the first place.

REACHABILITY GUARANTEE — KEY → PATH → GATED DOOR
Reachability chain from a key to its gated door A left-to-right chain: a Key, then a path connecting it, then a gated Door, illustrating that the key always sits somewhere reachable before the door has to be crossed. KEY PATH — always reachable first DOOR gated
GUARANTEE Every gated Door has a matching Key placed somewhere reachable along the path before that door has to be crossed — checked at generation time, not validated and patched afterward. Original schematic, not a reproduction of any game's level.

It's the same shape Resident Evil's map design has used since 1996, sometimes analyzed as "recursive unlocking": find a key, it opens a room, that room holds the next key, and the building opens up like a spiral. The difference is that in RE, that structure is hand-authored, room by room, by a level designer. Here, the same shape is a formal rule, checked automatically every time the layout regenerates.

Door and key is also just the current skin on it. The same guarantee holds for a hammer that breaks through a wall, a lever, a button that lowers a bridge over a moat — whatever the obstacle and whatever resolves it, the underlying rule doesn't change: never place the resolution somewhere the player can't reach first.

06 Rooms: space with intent

A room is a separate abstraction — an n-dimensional, definable area. It might be a hall between corridors, or a whole biome. What makes it a room instead of leftover space is that it carries its own unique scatter of objects, distinct from whatever rules govern the corridor around it.

Δ1 // Corridor
Governed by whatever rule applies to the surrounding cells — no scatter of its own, just the ambient rule passing through.
Δ2 // Room
Carries its own unique object scatter, independent of the corridor rule around it — what makes a bounded area a room instead of leftover space.

07 Why it's built this way, and what it looks like for an artist

Four things hold the system together: modularity, abstraction, scalability, and the ability to keep adding new abstractions without rebuilding what already exists. A wall, a chest, a door, a room — every one of them is registered the same way, which is what makes it possible to add a fifth or sixth category later without touching the first four.

In practice, the workflow is three steps:

  1. Register the entity — wall, room, whatever it is.
  2. Assign it a ruleset.
  3. Scatter it. Every scatter is one of three modes: manual, random, or a pattern-driven mix — wall A duplicated X times at a fixed step, for instance.
REGISTRATION PIPELINE — SAME THREE STEPS FOR ANY ABSTRACTION
Three-step registration pipeline shared by every module type A left-to-right chain of three steps — Register, Assign Ruleset, Scatter — the same pipeline applied whether the entity being registered is a wall, a chest, a door, or a room. REGISTER wall / chest / door / room ASSIGN RULESET zone / prop / structural SCATTER manual / random / pattern
One pipeline, every abstractionA wall, a chest, a door, a room — all three steps are identical regardless of which one is being registered, which is what makes adding a fifth or sixth category later a matter of reusing the pipeline, not rebuilding it.

Everything is fast to reconfigure, and supports variation on top of that — through a shader or a Blueprint, whichever fits the object. On the Unreal side, registration happens through one of two paths: a container managing HISM, for anything that repeats structurally, or an individual Blueprint Actor, for anything that needs its own behavior. That split is what keeps a wall variant and a functioning door from having to be the same kind of thing under the hood — the mechanism itself is covered in a separate post: Houdini to Unreal — Typed Placement Routing for HISM and Blueprint Actors. I also wrote a Houdini Digital Asset add-on to work with this registration live, which cut iteration time down considerably and got rid of the lag that used to come with editing it directly.

Above structural placement sits the gameplay layer — rooms, doors, keys, enemies — and it follows the same registration principle end to end, because that principle is the actual foundation of the tool, not a detail specific to walls. Above that sit props: benches, lampposts, whatever fills a space without changing how it plays.

08 Where this stands right now

This isn't a diagram — it's running. I'm building a game on top of this exact system: a maze-like city set in a late-1980s USSR aesthetic, with its own visual language and cast of characters. Gameplay scripting isn't finished, but the placement system already runs with real objects standing in for each abstraction — enemies, doors, rooms.

The placement system runs today with real stand-in objects for every abstraction — enemies, doors, rooms — not a diagram, not aspirational. Gameplay scripting on top of it is still in progress.

Current in-engine build, Soviet-era maze-city aesthetic, stand-in objects for enemies, doors, and rooms
FIG 03 — The current build running in-engine. Debug view: none — placement system output with stand-in objects for enemies/doors/rooms. Note these are functional stand-ins, not final art.
The maze generator cooking live, in-editor
FIG 04 — The generator cooking live, in-editor. Debug view: yes — this is the authoring tool, not gameplay. Note the placement system updating in real time as the layout regenerates.

09 What's next

This post is the shape of the thing. The next ones go into what makes it actually work: how a cell's content splits between shared instancing and individual Actors, how stacked rules get tuned without fighting each other — including one conflict I haven't solved yet — and the reachability check that makes the door/key guarantee real instead of aspirational.

Open, unsolved: what happens when two stacked rules in the same cell disagree — an action zone wanting high density against a puzzle rule wanting low density, for instance — has no resolution policy yet.

10 Result

The maze is a convenient, testable surface for a system that isn't really about mazes: cells that carry design intent, not just geometry; a grid borrowed from how real maps already work, with a layer of pacing borrowed from how shipped games already zone their levels; and one reachability guarantee reused under different names. Built by one person, for one person to keep shipping content without hand-placing every instance of it.

// END OF ARTICLE // CONTROLLED_PROCEDURALITY // EOF