Minimap + Tactical (Big) Map — Design¶
Date: 2026-06-27 · Slice: migration item #5 (UI/HUD). Ports the web minimap + full tactical map
(game.js renderMinimapOffscreen/drawMinimapPlayer/openBigMap) to Unity, structured so it drops
cleanly into the eventual full MVC UI layer.
1. Goal & scope¶
- Now: a corner minimap (always on) + a full-screen tactical map (tap the minimap to open), both showing the whole map fit to the view (web-faithful, top-down — NOT iso).
- Later (designed-in, not built now): zoom steps on the tactical map (1× fit-all → 2× → 4×) that show nearby surroundings with the player kept at screen centre.
- Non-goal now: the tactical-map legend panel polish, fancy icons, animations — minimal first.
2. Web reference (what to replicate)¶
The web bakes the map to an offscreen canvas (throttled ~5 fps) and blits it, drawing the player
marker live on top (smooth). It is a flat top-down projection of the tile grid (wx/TILE → px),
not the iso game view. Layers, bottom→top (renderMinimapOffscreen):
- Dark rounded panel background.
- Zone tints — a rarity-coloured rect per colored zone (this is the deferred "zone map-tint").
- Walls — dark-green pixels (
#33493a). - Dots: POIs (vault
#7a9bff/ other#caa85a), exits (kind colour; Descent Portal violet + bigger), chests — only if discovered (!opened && found), the closed vault gate, enemies — only if currently perceived (en.lit), and noise rings. - Player marker — drawn live each frame (pulsing), never baked.
Faithful gating to keep: chests show only once discovered, enemies only while perceived — both
already modelled in Unity (chest discovery latch; EnemyController.Shown/Perceived).
3. Architecture (MVC-ready)¶
Built under Assets/Scripts/UI/Map/ with strict Model / View / Controller separation so it slots into
the full MVC UI system later (migration §0/§3). Only the View touches a UI toolkit.
- Model —
MapViewModel(pure C#, no UnityEngine.UI). A per-refresh snapshot the view renders: the static tile grid ref (MapData) + lists of markers{ worldPos, kind, color, size }for player, perceived enemies, discovered chests, POIs, exits, the closed gate, and the colored zones (rects + tint). Built by the controller fromRaidState+MapData— reuses the existing perception/discovery state, no new gameplay logic. - View —
MapView(uGUI). Renders aMapViewModelinto aRectTransformviewport: aRawImageshowing the baked map Texture2D, plus a pool ofImagemarkers positioned by world→viewport mapping (§5). Stateless re: gameplay; given a model + a viewport it draws. Behind anIMapViewinterface so a UI-Toolkit reimplementation can replace it without touching model/controller. Two instances: the minimap (small corner rect) and the tactical map (full-screen rect). - Controller —
MapController(MonoBehaviour, injected). Owns: the baked texture (viaMapTextureBaker), open/close of the tactical map, the zoom level (later), and the throttled refresh. Builds theMapViewModeland pushes it to both views; handles the minimap-tap → open and the tactical-map close. In the full UI system this becomes a registered screen controller driven by theSignalBus. - Baker —
MapTextureBaker(pure).Bake(MapData, palette) → Texture2D: one pixel per tile (floor/wall/bush colours from web palette), point-filtered. Baked once per raid; re-baked only when the vault gate opens (the one tile change). The zone tints can be baked in or drawn as marker rects — bake them in (cheap, matches web layer order).
Toolset choice & rationale. uGUI (Canvas + RawImage + pooled Image) for the map surface,
not UI Toolkit. A baked pixel texture + many spatial markers maps directly to RawImage + Image and is
awkward in UI Toolkit. The migration plan already prescribes "uGUI + UI Toolkit mixed, pick per screen,
all MVC" — data/list panels (loadout, tuning) go UI Toolkit; the spatial map goes uGUI. The MVC split
keeps that decision swappable.
4. Coordinate math (the core)¶
World→normalized: u = worldX / (mapW·tileSize), v = worldZ / (mapH·tileSize) ∈ [0,1].
A viewport = { Vector2 centerUV, float spanUV } drives both modes uniformly:
- Fit-all (now): centerUV = (0.5, 0.5), spanUV = 1. The whole map fills the view rect
(aspect-preserved via the baked texture's own aspect, like web mmSize). RawImage.uvRect = (0,0,1,1).
- Zoom-centred (later): centerUV = playerUV, spanUV = 1/zoom (zoom ∈ {1,2,4}), clamped so the
window stays inside [0,1] only when not following — when following the player it stays centred and
the map texture scrolls under it. RawImage.uvRect = (centerUV − span/2, span); markers map the same.
Marker local position in the view rect:
local = ((markerUV − viewport.centerUV) / viewport.spanUV) · rectSize (then clamp/cull to the rect).
Fit-all reduces to (markerUV − 0.5)·rectSize. Same formula serves minimap and tactical map; only the
rect size and viewport differ. Player marker uses live position each frame; others throttled.
5. Minimap vs tactical map¶
| Minimap | Tactical (big) map | |
|---|---|---|
| Rect | fixed corner box (web MM_W, aspect-capped) |
full-screen panel |
| Viewport | fit-all (always) | fit-all now; zoom steps later |
| Always on | yes | opened on demand |
| Input | tap → open tactical map | tap outside / close button → close |
| Player marker | live, pulsing | live, pulsing (centre when zoomed) |
| Markers | perceived enemies, discovered chests, POIs, exits, gate, noise rings | same (+ legend panel later) |
mapEverOpened hint ("tap to open") until first open — port as a small one-time label (save flag later;
session flag for now).
6. Refresh & perf¶
- Texture: baked once (rebake on gate-open). Point filter, no mipmaps.
- Markers: player every frame (smooth); enemies/chests/POIs throttled ~5 fps (web cadence) via a
refresh timer in the controller. Marker
Images are pooled (show/hide, never per-frame alloc). - The tactical map only refreshes while open.
7. Future MVC transfer (the boundary)¶
Model + Controller are toolkit-agnostic; the View is the only uGUI surface (IMapView). When the full
UI/ MVC layer lands: MapController becomes a screen controller bound via SignalBus
(MapOpened/Closed, ZoomChanged), the MapViewModel becomes the observable screen model, and MapView
either stays (uGUI screen) or is reimplemented in UI Toolkit behind the same interface. No gameplay or
model code changes.
8. Implementation slices¶
MapTextureBaker—MapData→Texture2D(floor/wall/bush + zone tints), web palette. Verify via a RawImage.MapView+ world→viewport mapping — RawImage + pooled markers; fit-all. Render a static snapshot.MapController+MapViewModel— build the snapshot fromRaidState/MapData(perceived/discovered gating), throttled refresh, live player marker.- Minimap instance — corner rect, always on, tap-to-open.
- Tactical map instance — full-screen overlay, open/close.
- (Later) Zoom — viewport center=playerUV, span=1/zoom, zoom-step input. (Designed in §4; not built now.)
9. Out of scope (now)¶
Legend panel polish, custom icon art, the save.mapEverOpened persistence (session flag for now), and
the off-screen threat arrows (separate HUD item). Zone map-tint is included (it lives on the map).
10. Audit vs web — BUILT (2026-06-27)¶
Slices 1–5 done + verified in-game (MCP). Faithful to renderMinimapOffscreen/openBigMap:
| Aspect | Web | Unity | |
|---|---|---|---|
| projection | flat top-down tile grid | MapTextureBaker top-down Texture2D |
✅ |
| layer order | floor → zone tints → walls | same (Rarity.Color × zones.fillAlpha tint) |
✅ |
| palette | RAR_COLOR + inline hexes | Rarity.Color (identical) + MinimapStyle (web hexes) |
✅ |
| enemy dots | only en.lit (perceived) |
only PerceivedByPlayer |
✅ verified |
| chest dots | !opened && found |
!Opened && Discovered (the fog latch) |
✅ verified (opened/un-perceived excluded) |
| portals / vault / sealed gate | exit/POI/gate dots | same (progress=violet, safe=green, vault, gate) | ✅ |
| live player marker | drawn live (smooth) | own dot, every frame; markers throttled to refreshHz |
✅ verified mapped |
| fit-whole-map | mmSize aspect-fit |
MapView.FitFrame + MapViewport.FitAll |
✅ |
| tap minimap → big map → close | openBigMap/closeBigMap |
Button → Open / tap → Close | ✅ verified |
| re-bake on change | throttled rebuild | re-bake on vault-gate-open (the one mid-raid tile change) | ✅ |
Deltas / deferred (minor, flagged):
- Zoom — designed in (MapViewport.SpanUV, player-centred); not built (slice 6, "later" per the request).
- Noise rings on the map (web draws expanding pings) — not ported; NoiseSystem exists, easy add later.
- Player-dot pulse (web breathes the marker) — static dot for now.
- Tactical legend panel + the "tap to open" hint — out of scope now.
- Position — minimap is top-right (web is bottom-left) — a layout choice; the HUD's heal button sits
bottom-right, so top-right keeps it clear. Tunable.
- Close-on-any-tap (incl. on the map) vs web's tap-outside — simplification; refine when zoom lands.
Spawned via RaidBootstrap (DI) rather than a scene object — the VisionField bakes its fan mesh in the
editor, so any scene save drags that churn in; runtime spawn avoids it entirely.