This article continues the procedural environment series:
After generating the environment, routing its placements into Unreal and attaching lighting to those placements, I needed the next layer: turning generated geometry into identifiable places. The current system resolves building, entrance and apartment addresses from the relationships already present in the generated data.
Building 035 / Entrance 001 / Apartment 015 should identify the same place wherever it is used. The numbers should also follow an explicit spatial rule, as real-world addresses do, rather than being assigned independently to each facade.
I have always enjoyed exploring game worlds as much as following their stories and atmosphere. One thing that breaks that sense of exploration for me is an object that clearly suggests interaction but exists only as scenery. A door is the simplest example. Of course, this depends entirely on the type of game: not every environment needs every building to be accessible. But in an exploration-driven space, where locations themselves can carry value, making more of the environment structurally accessible can significantly change how the world feels. For this R&D project, I wanted to push that idea further. Buildings should not only exist as generated geometry; their entrances and apartments should also represent specific, identifiable places.
Suppose the player lives in Building 35, Apartment 15. A decal that says 15 is not enough. The system must be able to determine which entrance contains that apartment, which generated door represents it, and which building all of those elements belong to. The visible labels should be consequences of that structure rather than independent values typed onto meshes.
The trivial solutions are easy:
All three can produce something that looks like an address. None of them creates a reliable address structure.
Real-world numbering gives a useful model. Leeds City Council's policy, for example, assigns odd numbers to the left and even numbers to the right from the start of a street. This gives each side an ordered sequence. Local conventions differ; the diagram illustrates that policy, not a universal rule or a captured street.
The current project resolves buildings from authored anchors, order and a numeric step. Automatically classifying street sides is a possible extension, not implemented behaviour in this slice. Two ordered sides with different starting values and a step of two illustrate how such a numbering policy could work.
Randomness is useful for variation, but I do not want it to define the fundamental relationships of the generated environment. Even an invented addressing convention benefits from explicit rules. Once a few constants and relationships exist, the result becomes easier to reason about, query, debug and adapt to different level-design requirements.
The hierarchy below places apartment 015 in entrance 001 of building 035: 035 / 001 / 015.
Floors are an obvious spatial subdivision, but they are not yet a canonical segment in the current implementation. They can be introduced later if level design needs them to participate in address identity rather than remain a derived navigation property.
For example, with three apartments per floor:
Entrance 001
├── Floor 01 → Apartments 001–003
├── Floor 02 → Apartments 004–006
├── Floor 03 → Apartments 007–009
├── Floor 04 → Apartments 010–012
└── Floor 05 → Apartments 013–015
Procedural generation provides identity. The address layer assigns meaning.
A number plate displays an address; it does not own it. Other systems use the same canonical coordinate to identify the location.
The canonical value is ST_WorldCoordinate. It carries an ID, a seed and a list of typed address segments. Each ST_AddressSegment contains both:
"031";31, for ordering and logic.The distinction becomes especially important in a procedural environment. If a generated layout changes, I want to rebuild derived address state from the current placements rather than preserve stale labels or stale instance indices.
The address book itself is authored. At runtime, the manager copies that data into a working list and resolves generated placements against it. The authored asset remains a source of canonical intent; the runtime nodes are derived state.
This gives me two useful forms of control at the same time:
That coordinate sits above the existing placement pipeline. Houdini supplies identity and relationships; the Unreal address layer interprets them. This lets me add addressing without teaching the generator what a house number means.
The HDA does not decide that wall_island:7 is Building 031.
It publishes a semantic group and placement metadata. The address layer decides that this group represents a building and resolves the number.
Likewise, the scalar-data layer does not know what Display.Number = 31 means. It moves a value to the correct HISM instance. The material does not know what an entrance is. It only turns the value into digits.
The generator already publishes wall placements with a semantic group key such as:
wall_island:7
A group represents one ring of walls. The generic generator does not claim that this ring is a "house." That interpretation belongs to the address layer.
For the current project, a wall-island group becomes the unit of a building address.
This is more important than assigning a number to a single plate. A building can have many placements:
The address belongs to the group, not to one mesh.
Conceptually:
wall_island:7
│
▼
ST_BuildingNode
│
├─ group identity
├─ stable order
├─ optional authored anchor
└─ resolved building number
Authored anchors. Instead, the designer can bind a known address from the address book to one placement belonging to a building. That placement identifies the semantic group. The group then becomes an anchor for the resolver.
From there, neighbouring buildings can be resolved from their order and a configurable numeric step.
A simplified resolver looks like this:
ResolvedNumber =
AnchorNumber
+ (BuildingOrder - AnchorOrder) * BuildingNumberStep
The current implementation uses this ordered-anchor pattern. It is not intended to represent every municipal addressing convention; it is one deterministic policy built on top of a more general identity layer.
Why a stable placement key matters. The editor exposes an HISM instance index, but I do not treat that index as persistent identity.
An instance index is valid for the current arrangement of the component. After a procedural rebuild, instances can be reordered. Index 17 may now refer to a different placement.
The project instead carries an FMazePlacementHandle made from:
SourceHismName + PlacementKey
That is the same stability problem I encountered while building the lighting system. Address bindings need to survive the same kinds of rebuilds, so they use the same placement identity instead of treating array position as identity.
Ordering is not the group-key string. A plain text sort produces:
wall_island:1
wall_island:10
wall_island:2
The address layer therefore keeps a separate stable order for the building nodes rather than assuming that the semantic group key itself is a sortable address.

Its number is not resolved globally. It belongs to one building and receives an order inside that parent.
Each ST_EntranceNode stores the source placement, parent building group, procedural source order, resolved local order and final entrance number.
The resolver can be summarized as:
EntranceNumber =
count(entrances in the same building with lower source order)
+ 1
Source orders 12, 27, 33 and 40 resolve to entrance numbers 001, 002, 003 and 004: gaps in the source values do not create gaps in the labels.
Every building restarts from Entrance 001, because the comparison is scoped to one parent group.
Why I do not use world position. Sorting by world position would force the address resolver to decide which axis or direction represents the intended sequence for every building. A rotated or irregular structure would make that policy increasingly fragile.
The procedural source order already describes the intended order of the entrances. Using it keeps the numbering tied to generated identity rather than to one interpretation of world coordinates.
There is still a boundary condition: equal source-order values are ambiguous. The current "count lower values" rule gives equal ranks to equal values, so duplicates require validation or an explicit tie-break policy.
Apartment numbers continue across entrances in the same building.
Suppose two entrances contain 26 apartments each:
Entrance 001 → Apartments 001–026
Entrance 002 → Apartments 027–052
The local order of the first apartment in Entrance 002 is still 0. Its final number depends on something outside the currently loaded entrance:
ApartmentBase =
sum(ApartmentCount of every preceding Entrance in the same Building)
ApartmentNumber =
ApartmentBase + LocalApartmentOrder + 1
What if Entrance 002 is visited first?. If each entrance interior is loaded on demand, Entrance 001 may never have been loaded. But Entrance 002 still needs to know that its first apartment is 027.
Loading every interior just to count apartment doors would defeat the purpose of loading those interiors on demand.
Counting visible number labels would also be the wrong dependency. Presentation is not the source of truth for how many apartments exist.
The count belongs to the generated apartment-door geometry.
In the current entrance container:
HISM_Door_Apartment_A
is the source used to obtain the local apartment count.
Stage 1 — Know the count before loading the entrance. Each generated entrance variant has a preset JSON summary. A native helper reads the saved HISM instance count from:
generated_summary.instance_counts.HISM_Door_Apartment_A
The variant catalog associates that result with the entrance variant and level. At startup, the address layer converts the successful results into canonical entrance summaries and applies the counts to the corresponding ST_EntranceNode.
The address manager can therefore establish:
Entrance 001 count = 26
Entrance 002 count = 26
without first visiting either interior.
Stage 2 — Validate against the loaded geometry. When an entrance is actually loaded, its BP_Building reads the live HISM instance count and reports it through BP_LocationRoot.
For a loaded space, the runtime count is treated as authoritative.
If it differs from the preset summary, the system can warn, replace the stored count and refresh the affected numbering.
This lets the preset summary solve the preloading problem without treating cached generation data as permanently authoritative.
Unknown is not zero. This also forced me to distinguish two states that are easy to collapse accidentally:
Known count = 0
Unknown count
A confirmed empty entrance is valid data.
A missing JSON file, an invalid key or an unresolved earlier entrance is not the same thing.
HasApartmentCount exists specifically to preserve that distinction.
If the base number depends on an earlier entrance whose count is unknown, the resolver returns Found = false, clears unresolved numbers and waits for valid data rather than silently treating the missing count as zero.
That distinction is small in code, but important in practice: otherwise a missing count can produce plausible-looking but incorrect addresses.


A variant describes how an interior is generated; a coordinate identifies the place represented by that use of the variant.
Two addresses can use the same interior variant and the same preset data while remaining different logical locations.
For example:
Building 035 / Entrance 001 → Variant A
Building 035 / Entrance 002 → Variant A
This is important for procedural content reuse. Asset identity, generation preset and world identity should not collapse into one field simply because they sometimes point to the same data.
The seed follows the coordinate as part of the runtime address context, while the summary helper only reads the existing generation result. It does not start another generation pass merely to discover a count.
The address manager writes a generic scalar value:
Display.Number
to the resolved placement handle.
MazePlacementExtras maps the named scalar to a custom-data channel on the correct HISM instance:
The material reads the channel and turns the value into digit tiles.
Canonical relationship
↓
Resolved address number
↓
Display.Number
↓
MazePlacementExtras scalar override
↓
HISM PerInstanceCustomData
↓
Address material
↓
"031"
The address layer interprets the number; Extras transports it, and the material renders it.
That is the same separation of responsibility used in the lighting work: one placement identity can drive a completely different type of engine-side data without changing the generator.
The digit material. The current material accepts a number from 0 to 999 and uses a horizontal ten-digit texture atlas.
It supports two input paths:
PerInstanceCustomData for HISM labels;Both routes feed the same digit-selection logic.
A three-digit display is a presentation limit, not an address-model limit. Supporting larger values would require a wider material implementation, not a different canonical coordinate system.

When a plate shows 000. One useful diagnostic case is a label displaying 000.
That does not prove that the resolved address is zero. In the current material setup it usually means that the expected instance value did not reach the material.
The diagnostic path is therefore explicit:
SourceHismName + PlacementKey resolve to exactly one current instance?Display.Number?
M_AddressDigits_3: the number enters on the left, the custom node turns it into atlas UVs, the atlas sample leaves as emissive color and opacity mask. Debug view: yes — Unreal Material Editor, not a rendered result.The current slice resolves building numbers from authored anchors, entrance numbers from their order inside a building, and apartment ranges from generated door counts. Preset summaries make those counts available before an interior is visited; loaded geometry checks and corrects them.
The documented first-visit checks cover both entrances in the current 26 + 26 case: visiting 002 first still yields apartments 027–052. The same coordinate now also supports travel into addressed interiors, but the loading and return lifecycle belongs to the next article.
A semantic wall group is not universally a building. The current implementation interprets one wall-island ring as a building. An outer block and an inner courtyard ring may therefore appear as separate groups even if level design wants them to share one address.
That requires a merge rule or a different grouping abstraction. The address layer should not pretend the generator gave it semantic information that it did not.
An authored anchor is still authored. The system derives numbers around anchors. It does not decide which canonical address a level should start from.
Runtime display values are not persistence. The scalar overrides used for display are recomputed at runtime. They are not a save-game system.
Persistent world-state changes would need their own persistence layer.
Source order must be unambiguous. The entrance resolver assumes that source order establishes a rank. Duplicate values need validation or an explicit tie-break rule.
Apartment ranges require valid earlier counts. A later entrance cannot have a trustworthy cumulative base if the count of a preceding entrance is unknown.
The preset-summary system solves that dependency for the currently generated variants, but invalid or missing summaries correctly leave the range unresolved.
Floors are not yet part of the canonical coordinate. They are a likely extension, not a shipped address segment in this slice.
The next spatial subdivision I want to explore is floors. They can begin as a navigation property; whether they become an address segment depends on what level design needs to identify. Rooms can follow the same principle when a character or script needs a specific destination inside an apartment.
I also want to develop a library of several hundred entrance, apartment and room presets. That is a later iteration, not the current catalog size. A preset controls the representation of a location; its coordinate preserves which place that representation belongs to.
The numbering policy can evolve as well: odd/even street sides, district offsets or floor-aware rules are possible extensions. They are not implemented in this slice. Authored anchors and procedural relationships would still provide the control underneath them.
Generated groups and placements now resolve to a shared ST_WorldCoordinate. Authored anchors control building numbers, local order controls entrance numbers, and cumulative door counts control apartment ranges—even when earlier entrances have not been visited.
The HISM label displays one segment of that coordinate. Other systems can use the full address without defining the location again.