9.1 KiB
Map Pipeline
How to make a new map, and what the generator puts in the scene for you.
One-time setup
Open 9Player and run TD → Map Pipeline → Build Core Prefabs From Open Scene.
This reads the fully-wired reference scene and writes:
| Output | What it holds |
|---|---|
Assets/_Project/Prefabs/MatchCore/MatchServices.prefab |
Every map-independent, non-networked service |
Assets/_Project/Prefabs/MatchCore/MatchNetwork.prefab |
The six scene-placed NetworkBehaviours |
Assets/_Project/Settings/MapTemplate.asset |
Prefab references, terrain defaults, starter-layout proportions |
It copies live components rather than rebuilding them from an asset scan. AssetDatabase.FindAssets
order isn't guaranteed stable across machines or re-imports, and several of these arrays double as
network identity — EnemyUpgradePool.options indices are card network ids, and
TowerPlacementManager.towerDefinitions indices are TowerTypeId. Two people generating the same
map could commit different orders, and that desyncs a host from a client. Copying sidesteps it, and
gives each new map the same known-good content as the reference.
Maps having different catalogs from each other is fine and supported — every peer loads the same scene, and
PlayerTowerDeck.ServerInitializeclears the deck at match start. Per-map enemy rosters are a matter of assigning a differentRunDefinitionon_MatchNetwork/RunState.
The reference scene's contents aren't modified, but Unity marks it dirty. Don't save it.
Re-run this any time the shared boilerplate changes in a way the prefab should pick up.
Making a map
TD → New Map...
Fill in name, player count, and size. The dialog validates the starter layout live and tells you the exact width or height to use if the shape can't hold one.
On Create it does all of this:
- Creates and saves the scene in
Assets/_Project/Scenes/Levels/ - Instantiates
_MatchServices(linked prefab instance) and_MatchNetwork(unpacked) - Creates the
LevelDataasset and assigns it to bothLevelAuthoring.targetAssetandLevelLoader.level - Generates a bakeable starter volume layout
- Creates a
Terrainon theTerrainGeometrylayer, sized to the map plus a skirt - Registers the scene in Build Settings
- Appends the
LevelDatato theMapRegistryinMainMenu.unity - Runs the bake
Steps 3, 6 and 7 are the ones that used to fail silently — a map missing any of them looks fine in the editor and breaks only at match start, or just never appears in the lobby.
A generated map bakes with zero warnings as long as you filled in author and description.
What you get, and where to edit it
Four roots instead of the ~30 the hand-authored maps carry:
_LevelAuthoring ← yours: shape the volumes here
_MatchServices ← shared prefab: don't edit per-map (except LevelLoader.level)
_MatchNetwork ← the six NetworkBehaviours, unpacked
Terrain ← yours: sculpt here
_MatchServices is a linked prefab instance, so fixing something in
MatchServices.prefab fixes it in every map at once. The one field that is legitimately a
per-instance override is LevelLoader.level.
Never press "Apply All" on the _MatchServices instance — that would push this map's
LevelData into the shared prefab.
_MatchNetwork is unpacked deliberately: NGO treats in-scene placed NetworkObjects that are prefab
instances differently from loose ones, and unpacking keeps generated scenes structurally identical to
hand-authored ones. The cost is that changes to MatchNetwork.prefab don't propagate to maps already
created — there are only three asset references in it (RunDefinition, GoldConfig,
EnemyScalingConfig), so fix those by hand if they ever change.
The starter layout
Player bands stacked south → north, full map width:
┌─────────── Goal ───────────┐ north
│ Player N band │
│ ····· leak gate ····· │
│ Player N-1 band │
│ ····· leak gate ····· │
│ Player 1 band │
│ [Player 1 spawner] │ south
└── MapArea (margin border) ─┘
Enemies spawn at the south edge of their owner's band and walk north. Each band leaks through a one-tile gate into the next; the northernmost band is goal-adjacent instead of having a leak.
Proportions (margin, band depth, gate width, goal size, terrain skirt and carve depth) all live on
MapTemplate.asset — tune them there, not in code.
Build Access: one maze each, or one maze together
_LevelAuthoring → Rules → Build Access is a per-map choice. Also offered in the New Map dialog.
| Mode | Meaning |
|---|---|
OwnerOnly |
Default. A player builds only inside their own PlayerZoneVolume. |
Shared |
Any player builds on any Buildable tile — the whole map is one cooperative maze. |
Ownership doesn't disappear in Shared mode. OwnerGrid still drives leak attribution, camera
start position, builder spawn point, and minimap tinting; zones still own their spawners and leak
exits. The only thing that lifts is the placement restriction. That's why this is a per-map enum and
not a change to how maps are authored — a Shared map is authored exactly like an OwnerOnly one.
Towers still belong to whoever paid. Sell, upgrade and paint stay owner-gated in both modes, so a teammate can't sell your tower out of the lane you put it in.
Both modes route through LevelLoader.HasBuildPermission, which the client's ghost preview and the
server's authoritative check both call — they can't drift apart and show a white ghost that then gets
rejected.
Shared mode widens path validation. Under
OwnerOnlya player can only strand their own spawners, soTowerPlacementManager.CheckPathValiditychecks one zone. UnderSharedanyone can wall off anyone's lane, so it checks every zone's spawners — otherwise a player could leave a teammate's enemies with no route to any exit. Cost is one BFS per spawner per placement attempt rather than one per placement; placements are already throttled torequestsPerFrame.
Changing the mode on an existing map just needs a re-bake.
Leak exits are optional on Shared maps
Bake rule P5-8 ("every zone must have an outgoing leak or be goal-adjacent") is skipped on
Shared maps, so you can delete the LeakExitVolumes entirely. Every other leak rule is conditional
on a leak existing, so they're vacuously satisfied at zero.
That's safe because leak exits do almost nothing at runtime. PathfindingService.ComputePath takes no
destination — enemies always path to LevelData.Goals, and the route through a gate is emergent
from walkability, never directed. LeakExitData.NormalizedWeight is written by the bake and read
nowhere. Leak attribution runs off OwnerGrid, not leak volumes, so it keeps working. The safety
property P5-8 protected is enforced more strictly by P5-4, which BFSes every spawner to the exit
set — and with no leaks present that set is the goal tiles, which is exactly the right requirement.
Zones must then be directly adjacent. A
LeakExitVolume's tiles are walkable; a bare gap between two zones is not. Delete the leaks without closing the gap and the bands become disconnected — P5-4 hard-errors and P5-11 warns, so the bake catches it loudly rather than shipping a map whose enemies can't move.
New Map handles this for you: choosing Shared generates bands butted directly together with no gate rows and no leak volumes. Choosing OwnerOnly generates the one-tile gates.
Changing the Quick Play map
Quick Start skips the lobby and loads MapRegistry.Default, which is the first valid entry in
the Maps array — not a separate setting.
Open MainMenu, select the MapRegistry GameObject, and drag the map you want to element 0.
Entries with an empty MapName or ScenePath are skipped, so "first" means first usable.
New maps are appended to the array, so creating one never silently changes what Quick Play loads. Reordering is deliberate.
Things worth knowing
- Walkability is the union of the gameplay volumes, not something you paint. Walls are the
absence of a
PlayerZone/Spawner/LeakExit/Goalvolume.MapAreaVolumetiles are deliberately not walkable — that volume only bounds camera pan and builder movement. - Terrain must stay on the
TerrainGeometrylayer. The builder's ground raycast and spell targeting both filter on it; onDefaultthe builder silently walks at Y=0. - Terrain visuals have no relationship to the grid. Nothing validates that the hills you sculpt match the volumes you placed. That correspondence is yours to keep.
- Volumes must be axis-aligned and unit-scaled. Rotation is a hard bake error; non-unit
Transform.Scaleis a warning. Size volumes withBoxCollider.Size, or use the Resize Volume Edge scene tool. MatchRules.MaxPlayersis 3. Larger player counts bake fine but no lobby can fill them yet.