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

pavel-zosim: code art automate●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. Finding a path through a maze is a different problem from deciding what the player encounters along it. 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.

This is also a procedural environment generation pipeline. Structural modules assemble walls, floors, ceilings, and building facades; prop scatter adds objects such as streetlights, benches, and snow; Unreal-side rules layer lighting and other details onto the placed instances. Zone and gameplay rules give those spaces a purpose. The same pipeline supports both environment assembly and level design, with manual control over individual placements.

Houdini Visualization Primitives debug view -- colored oriented boxes for every structural, gameplay, and prop placement in one generated maze
FIG 01 — Four of the generator's own debug/visualization views inside Houdini, same generated maze. Debug view: yes — these are authoring-time views, not final art.
The generated maze layout cooked into Unreal, aerial view of the modular buildings and streetlights
FIG 02 — The same generated layout, cooked into Unreal and running in-engine. Debug view: none — final placement output, not the Houdini authoring view.

The current demo uses straight wall segments meeting at right angles. This keeps the module set and junction rules manageable while I develop the placement system. The next step is non-orthogonal geometry: more complex modules, more flexible placement steps, and wall and floor scatter that can follow the resulting shapes. Section 09 describes that direction.

Placement rules accept both random selection and manual entries. In FIG 03, a manual override replaces one wall column with an entrance. Vertical and cap patterns can layer over the base scatter, while weights on the authored variant pools control how often each option is chosen.

A wall run built entirely from window and blank panel modules, before any manual override, Unreal Editor with the Vertical Override panel open
FIG 03 — A blank window wall, then the same run with one column manually overridden to an entrance. Debug view: yes — Unreal Editor with the Vertical Override panel open, same building as FIG 02.

02 Borrowing structure from real maps

A map grid provides a useful model for addressing space. In the Military Grid Reference System (MGRS), a grid-zone designation and a 100 km square identifier locate a broad area; paired easting and northing digits refine the reference, down to 1 m with five digits per axis. This describes coordinate precision, not a guarantee of measurement accuracy.

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 design intent. In an interview with Phil Co, the Valve level designer describes planning puzzle and combat beats in relation to adjacent sections, then revising them through playtesting. His concrete example comes from Half-Life 2: Episode Two, where combat was added near the bridge and car sequence in response to playtest feedback.

“We might have an initial idea that we want a certain level to contain a puzzle or combat.” — Phil Co, Interlopers interview

Resident Evil offers another example of pacing through space. In an interview with PC Gamer, Resident Evil 2 remake director Kazunori Kadoi describes save rooms as relief from sustained tension and as bases for exploration. In Resident Evil Village, Capcom presents the Duke as a merchant for buying, selling, and upgrading weapons. I use those rest and resupply functions as references for a shop zone; a merchant location and a safe room are not automatically the same thing.

The connection to my generator is a design analogy: spatial subdivision locates content, while zone rules express its intended role. This does not imply that either game uses my cell model or a literal square grid. For a broader discussion of beats and zones, see The Level Design Book: Pacing.

TWO LAYERS OVER ONE GRID — LOCATION VS. INTENT
A floor plan used for location versus the same floor plan carrying design intent Left panel: a small floor plan of four rooms connected by corridors, drawn with no meaning attached, just structure. Right panel: the identical floor plan, now with each room labeled by design intent — shop, action, or breather. LAYOUT — LOCATES SHOP ACTION BREATHER ACTION INTENT — DECIDES ROLE
Same subdivision, two jobsThe same illustrative floor plan appears twice: first as connected spaces, then with intended roles assigned. This is my conceptual model, not a map from Half-Life 2 or Resident Evil and not a diagram of the MGRS standard.

03 The cell as the unit of intent

FIG 04 — A generated level running in the current build, demonstrating the layout working end to end. Debug view: none. Environment art and lighting are pre-alpha, not final. 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.

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.

FIG 05 — SAME GRID / DIFFERENT ZONE RULES
The same cell assigned a shop rule or an action rule Two identical three-by-three grids. The center cell on the left is marked S for shop; the corresponding cell on the right is marked A for action. Cell size, position, and neighboring cells are unchanged. Only the zone assignment differs. This is a conceptual diagram, not a capture of generated output. SHOP ACTION S A ↔ Rest / resupply Encounter / pressure Same cell size, position, and neighbors
S / ShopA pause in the level's pacing: room to breathe and replenish ammo or health.
A / ActionAn encounter beat: enemy placement and behavior give the same spatial slot a different purpose.
CONCEPT Only the zone rule changes in this comparison. Structural and prop rules remain separate inputs. The grid shows cell boundaries, not walls; the letters mark zone assignments, not placed objects.

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.

FIG 06 — ONE SPACE / TWO PLACEMENT ROLES
Obstacles and actions in one example space A top-down schematic shows a player route bending around a wall. A lamppost provides a landmark. An enemy and a chest mark gameplay placements whose behavior is handled by Blueprint logic. Numbers identify each example in the legend. This is a conceptual illustration. 1 2 3 4 ENTRY Illustrative route through the same space
Obstacles / spatial role1 — Wall: blocks movement and makes the route turn. 2 — Lamppost: provides a landmark that helps the player orient themselves.
Actions / gameplay role3 — Enemy: a placement whose encounter behavior is handled by Blueprint logic. 4 — Chest: a placement whose interaction and loot are handled by Blueprint logic.
CONCEPTThe same area combines spatial guidance and gameplay placements. Dashed lines mark cells; the arrow shows an example route. This is an explanatory schematic, not a captured layout.

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 along a winding corridor A winding corridor drawn as a line with four right-angle turns, from the player's start position, through a key placed along the way, to a gated door at the far end, with a locked area beyond the door that the key/door pair blocks off. LOCKED DOOR gated PLAYER KEY PATH — always reachable first
GUARANTEE Generation constraint: the matching key must be reachable while its door is still locked. This schematic illustrates the intended progression; it is not a validation result from a generated layout.

A useful comparison is Chris Pruett’s analysis of “Recursive Unlocking”: returning to earlier areas with newly acquired items gradually opens more of Resident Evil’s mansion. His analysis follows a Resident Evil: Director’s Cut speedrun in Arrange Mode. It describes a traversal pattern, rather than documenting Capcom’s generation algorithm or the behavior of a typical first-time player. My system applies a related dependency principle to generated layouts.

The same dependency principle could support a hammer and breakable wall, a lever, or a button-controlled bridge: the player must be able to reach the means of opening a route before that route is required. Those are possible extensions of the lock/key model, not additional mechanics demonstrated here.

The generation rule and the debug display have different jobs. The rule requires a matching key to be reachable before its locked door must be crossed. Matching indices establish which door a key opens; reachability also depends on the layout's connections and which gates are still closed at that point in the progression.

The debug view below helps inspect the generated result. It overlays the Entry → Exit progression route and color-coded Key/Door pair connections, making unexpected pairings easier to spot. A line connecting a key to a door does not, by itself, prove that the player can reach the key. That guarantee comes from the generation logic operating on the layout graph; the overlay is a diagnostic view of its output. This is a graph-level guarantee: collision, navigation, and interaction behavior in Unreal still need to preserve those connections and be tested in-engine.

Debug schematic showing the entry-exit progression route and color-coded key-door path pairs over a generated dungeon layout
FIG 07 — Diagnostic overlay: the progression route and color-coded Key/Door pair connections. Pair connections show associations; they do not independently prove reachability. Captured from the same earlier prototype as the Gameplay scatter figure in §07, not the 3.4 HDA covered elsewhere in this post. Debug view: yes.

06 Rooms: space with intent

A room is a separate abstraction — a defined region with its own placement rules. 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.

A courtyard playground is a direct example: it's a Ground Prop like any other, but its Topology Filter allows only the Room category, so it only ever lands inside a Room cell — placed there Manually, locked to a specific Anchor Index in this build, rather than left to Random. The same filter runs the other way just as easily: a streetlight in the same registry can be restricted to one topology type instead — L-corners only, say — so it shows up exactly at street corners and nowhere else.

A courtyard playground rendered next to the Ground Prop Scatter UI, Topology Filter set to Room only, Mode Manual with a locked Anchor Index
FIG 08 — A playground scattered only into Room-type cells, next to an aerial view of several Room cells inside one courtyard block. Debug view: first slide yes (HDA panel), second shows the in-engine work in progress.

Room isn't just a density flag, either — its own Room Type is a registered category (treasure, boss, regular, and so on, the same registration pattern as everywhere else), and that type is what other systems key their spawn rules off: a boss enemy set to spawn only in boss-type rooms, a specific reward or key set to only ever land in a treasure-type room, all through the same Manual/Random/Both choice as everywhere else. The same idea reaches into a room's own interior, too — specific cells inside it can carry their own spawn rules, and the walls bounding it can pull from a room- or biome-specific set of decals and wall variants instead of the level's general pool.

Schematic debug view of four registered rooms color-coded by Room Type -- treasure, regular, regular, and boss
FIG 09 — Four registered Room Types, one color per type in the schematic; with assets, each room pulling its own decoration set. Captured from the same earlier prototype as the other debug-view figures in this post, not the 3.4 HDA covered elsewhere. Debug view: yes.

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. Scatter modes are Manual, Random, and Both. Where supported, patterns additionally control module sequences and repetition — wall A repeated 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 shows icons for a wall segment, a chest, a door, and a room. Assign Ruleset shows a generic object with three rule tags attached. Scatter shows the same kind of object placed multiple times across a small area. REGISTER wall / chest / door / room ASSIGN RULESET zone / prop / structural SCATTER manual / random / both
One pipeline, every abstractionA wall, a chest, a door, a room — the same authoring sequence applies, while each domain retains its own registration fields, placement targets, and constraints.

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. All three sit on the same Register → Scatter pipeline from the schematic above; what changes between them is what gets registered, what the scatter step targets, and how much built-in behavior — if any — comes attached to the result.

Module: the structural basis

Module is the base layer — walls, ceiling, and everything the floor is built from. Registering a module reserves a name for it in the container, and that name is exactly what the scatter step looks up later — nothing gets placed unless it was reserved first. Ground modules come in five topology types distinguished by connection count and arrangement — I, L, T, U, X — each its own weighted pool of variants, so a straight corridor tile and a four-way crossing never compete for the same pool. Walls split into two kinds: standard panels along a run, and caps at every dead end or opening. Ceiling reuses the exact same five-type registration as the floor, on a per-floor toggle — nothing stops a ceiling module from being a chandelier or a set of stalactites instead of a flat panel; that's a game-design choice, not a limit of the system.

A top-down generated maze with I, L, T, U, and X ground topology crossings, next to the Data Registry listing Ground and Wall structural types by topology and their HISM component names
FIG 10 — A generated maze, top-down, showing the I/L/T/U/X ground topology in the corridor crossings, next to the actual Data Registry entries (Structural Type, Topology Type, HISM Component Name) behind them. Debug view: yes — registry panel plus a plain top-down viewport, not final art.

Floors stack, and each enabled floor gets its own height and its own pattern — expressed with the same small grammar, whether it's driving a wall's vertical module sequence or a wall/cap run along one floor:

Rigid:              [A], [A-B], [A][B], [A-B-A]
Rigid (repeated):   [A]3, [A-B]2
Fill (repeated):    <A>, <A-B>, <A-B-A>
Variations:         [A*], [B*], <A-B*>, [A*-B]
Incorrect:          <*>, [*], <A>2, <A><B>, having no Fill modules

A and B represent Building Modules

Square brackets pin an exact, rigid sequence; angle brackets repeat and fill whatever space is left. A number after a bracket repeats that block a fixed number of times, and an asterisk pulls from that slot's own weighted variant pool instead of one fixed pick — so a pattern can stay structurally rigid while still varying which exact module fills it every time it runs. Every pattern is overridable on top of that: pick a specific column manually, or let the override roll randomly with its own count — either way it rewrites that one column's module or height sequence without touching the pattern the rest of the wall still follows (the FIG 03 example above is exactly this: a manual override rewriting one column of an otherwise pattern-driven wall).

A real example, straight from this build's own Floor Pattern field: [Floor_1] — a rigid, one-off ground floor, then two floor treatments that repeat to fill however many stories the building ends up with. Four floors are configured underneath that: the ground floor sits at a lower 0.6m plinth height running a plain panel wall and cap, and the three floors above it each run at 2.9m with a richer wall pattern mixing windows and balconies into the run. Variety doesn't stop at the module level, either — this build drives curtain position inside the window modules through a vertex shader, one more layer of visual variation riding on top of a system that otherwise only knows about modules and patterns.

Wireframe view of a wall run built from the Floor Pattern config, next to the Pattern Walls and Floors UI showing the Floor Pattern field and four configured floor entries
FIG 11 — This build's actual Floor Pattern config, next to the wireframe result — note the denser triangulation on window and balcony modules versus the plain panels around them. Debug view: yes — wireframe render plus the authoring UI, not final art.

Gameplay: the same registration, real behavior on top

Doors, chests, keys, traps, enemies, loot, and rooms all register through the identical Module-style entry — pick an abstract type, assign a representation, done. What's different is that the tool itself already knows what each of these types means: a Door checks the reachability guarantee from §05, a Key carries a lock ID, an Enemy spawn carries its own density and pairing rules. Scatter here doesn't walk a wall run — it picks a point on the floor and drops the registered entity there, Manual or Random exactly like everywhere else, including the player's own start point, which is registered the same way as everything else. Density is a first-class option too: nothing stops one cell from spawning several enemy types at once, each with its own variant weighting, instead of exactly one enemy per cell.

A Trap doesn't have to sit on a single point, either — the same registration can claim an area spanning several cells at once, set through a size parameter, so one trap reads as a hazard zone instead of one tile. And a spawn point isn't necessarily atomic: the cell it occupies can be subdivided into several sub-cells, each carrying its own Blueprint or object, for finer control over what actually spawns where inside that footprint — a Room with a loot pile in one corner and a guard standing in another, both still coming out of the same registered spawn point.

Schematic debug markers over a generated dungeon layout -- flat colored zones for traps, doors, and loot
FIG 12 — Trap, room, and loot scatter, schematic and with proxy assets. Captured from an earlier prototype in the same project rather than the 3.4 HDA covered elsewhere in this post — used here because the zoning reads more clearly on this simpler setup. Debug view: yes.

Props: decoration with placement filters

Props add decoration without the lock/key dependencies of the Gameplay domain. Their placement can still be constrained by topology filters, target surfaces, and manual anchor selection. A streetlight, a bench, a poster — anything that fills a scene without changing how it plays — registers freely and scatters directly on top of an already-placed Module surface: wall, floor, or ceiling. The scatter step carries the same Manual/Random/Both choice as everywhere else, plus its own offset, rotation, mesh/material variant, and scale randomization, each on its own seed. And it can be scoped as tightly as it needs to be — restricted to one specific room or biome, or to a single topology type, like only the T-junctions in a floor's corridor network.

A concrete pair from this build: a snow pile prop, carrying two mesh variations, with its Topology Filter narrowed to straight segments only, so it only ever settles along a plain corridor stretch — and a streetlight, filtered the opposite way, to L-corners only, so it only ever stands at a turn. Same registry, same scatter step, two unrelated placement rules living side by side. None of this is unique to ground props, either — Wall Props filter by the same kind of topology (corner, straight, cap), and Manual mode on any of these systems can lock one exact point instead of a whole category, down to a single Anchor Index. Stacked together, that's enough to direct a genuinely deliberate moment — snow gathering only in dead corners, a streetlight only at junctions, one hand-placed prop marking one specific spot — instead of every scene reading like the same generic scatter pass.

A snow pile prop and a streetlight in a generated street, next to the Ground Prop Scatter UI showing their opposite Topology Filters -- straight-only for snow, L-corner-only for the streetlight
FIG 13 — Snow (straight segments only) and a streetlight (L-corners only), same registry, opposite Topology Filters. Debug view: yes — HDA panel visible alongside the rendered result.
ModuleGameplayProps
CoversWalls, caps, ground, ceilingRoom, Door, Chest, Trap, Enemy, Loot, KeyStreetlights, benches, decals — anything decorative
Scatter targetsCell / column, by topology typeA point on the floorA Module surface — wall, floor, or ceiling
Built-in behaviorNone — pure geometryDomain rules baked in (reachability, lock/key, density)None — pure decoration
RepresentationContainer/HISM or Blueprint ActorContainer/HISM or Blueprint ActorContainer/HISM or Blueprint Actor

MazePlacementExtras: syncing lights, materials, and objects to the same instances

One more layer sits entirely on the Unreal side, independent of Houdini: MazePlacementExtras, a Blueprint component that attaches rules directly to an already-registered HISM — pick a Registered Module (a window panel, say) in the Blueprint and hand it a rule, no round-trip through the generator. Each rule picks an object type — a light, a decal, a particle system, snow, or a custom Blueprint — sets its own probability and seed, and applies a local offset relative to the instance it's attached to.

Selection can run two ways: by raw instance order, or by a Stable Placement Key — a deterministic pick hashed from that placement's own domain, key, and seed, so which specific windows end up lit stays the same even if the underlying HISM's instance order shifts on a later re-cook. For lights specifically, every rule carries its own color, intensity, attenuation radius, source size, and shadow toggle, so a window-lighting rule and an entrance-lighting rule on the same building can look completely different without the two ever touching each other. Clearing and reapplying extras follows the same idempotent pattern as the rest of the pipeline — re-cooking never doubles up a light that's already there.

Lighting is just the case this section keeps using as an example — the actual mechanism underneath is a plain deterministic 0/1 draw per instance, and that draw doesn't know or care what it's feeding. The same interior rooms visible through a lit window come from a separate cubemap-array material with its own randomizer wired the same way, picking which fake room shows up behind which window independently of whether that window is lit at all. Wire that same 0/1 output somewhere else and it drives a decal, a particle system, or which variant of a custom Blueprint spawns — the light rule is one consumer of a generic signal, not a special case baked into it.

The MazePlacementExtras component in BP_Building, showing a Rules array with light parameters for a window HISM
FIG 14 — The rule config, the interior-cubemap material it pairs with, and the result on the street. Debug view: first two slides yes (editor UI), last two show the in-engine work in progress.

08 Where this stands right now

I'm developing a game on top of this pipeline: a maze-like city with a late-1980s USSR aesthetic. The generator provides the layout and placement foundation; gameplay scripting and the game's visual identity are still in progress.

I'll share development updates, in-engine results, and further breakdowns of the tools here on my blog as the project develops.

09 What's next

The next development goal is to move beyond the current 90-degree layout and support more complex generated structures. That means extending the module system, the placement step, and the way walls and floors are scattered.

These are development goals. The examples in this article show the current orthogonal system. I'll publish experiments and results on the blog as this work progresses.

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.

FIG 15 — A visual walkthrough of the designer-controlled procedural dungeon framework: changing the seed and adjusting generation settings, using a Houdini Digital Asset integrated into Unreal Engine. No spoken commentary. Watch on YouTube.
PROJECT MAP // RELATED WRITE-UPS
  • [01] CURRENTControlled Procedurality — How a Maze Generator Became a Level Design System
  • [02] LIVEHoudini to Unreal — Typed Placement Routing for HISM and Blueprint Actors — the registration split (container/HISM vs. individual Blueprint Actor) mentioned in §07, covered end to end.
    pavelzosim.com/post/houdini-unreal-typed-placement-routing
  • [03] LIVEThe Light System — Randomized and Synchronized Lighting on Generated Geometry — the MazePlacementExtras layer mentioned in §07, covered end to end: sockets, stable selection, the emissive mask, and the scalar data layer.
    pavelzosim.com/post/lighting-generated-geometry
  • [04]LIVEProcedural Address Resolution — From Placement Identity to World Coordinates — deterministic building, entrance and apartment addresses resolved from placement identity.
    pavelzosim.com/post/addresses-on-generated-buildings
  • [05]LIVEProcedural Travel Resolution — From World Coordinates to Loaded Interiors — address-aware portals, reusable interiors, arrival anchors and return routes.
    pavelzosim.com/post/procedural-travel-resolution
// END OF ARTICLE // CONTROLLED_PROCEDURALITY // EOF