188 lines
9.1 KiB
Markdown
188 lines
9.1 KiB
Markdown
# 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 `NetworkBehaviour`s |
|
|
| `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.ServerInitialize` clears the deck at match start. Per-map enemy rosters
|
|
> are a matter of assigning a different `RunDefinition` on `_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:
|
|
|
|
1. Creates and saves the scene in `Assets/_Project/Scenes/Levels/`
|
|
2. Instantiates `_MatchServices` (**linked** prefab instance) and `_MatchNetwork` (**unpacked**)
|
|
3. Creates the `LevelData` asset and assigns it to **both** `LevelAuthoring.targetAsset` *and*
|
|
`LevelLoader.level`
|
|
4. Generates a bakeable starter volume layout
|
|
5. Creates a `Terrain` on the `TerrainGeometry` layer, sized to the map plus a skirt
|
|
6. Registers the scene in **Build Settings**
|
|
7. Appends the `LevelData` to the **`MapRegistry`** in `MainMenu.unity`
|
|
8. 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 `NetworkObject`s 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 `OwnerOnly` a player can only strand their own
|
|
> spawners, so `TowerPlacementManager.CheckPathValidity` checks one zone. Under `Shared` anyone 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 to `requestsPerFrame`.
|
|
|
|
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 `LeakExitVolume`s 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` / `Goal` volume. `MapAreaVolume` tiles are
|
|
deliberately **not** walkable — that volume only bounds camera pan and builder movement.
|
|
- **Terrain must stay on the `TerrainGeometry` layer.** The builder's ground raycast and spell
|
|
targeting both filter on it; on `Default` the 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.Scale` is a warning. Size volumes with `BoxCollider.Size`, or use the
|
|
**Resize Volume Edge** scene tool.
|
|
- `MatchRules.MaxPlayers` is **3**. Larger player counts bake fine but no lobby can fill them yet.
|