Presentationcom.zoa.minimap · v0.1.0

ZOA Minimap

Tag a GameObject, and it appears on the minimap, the compass and the world map without anyone wiring anything.

Spatial HUDs have an awkward data problem: the things worth showing on a minimap are owned by every system in the game and by none of them in particular. Vendors come from the economy package, hostiles from the AI package, objectives from the mission system, waypoints from the player. Asking each of those to know about a HUD widget is how presentation packages end up depending on gameplay ones.

The answer here is a marker component and a static registry. Drop a MinimapTrackable on anything the player should see, and it registers itself on enable and unregisters on disable. The HUDs query the registry each frame and transform every trackable's world position into their own UI space. No producer references a HUD, and no HUD references a producer.

Three surfaces consume that registry. MinimapHud draws a circular top-down map that either rotates with the player or stays north-up. CompassHud draws a horizontal strip where cardinal ticks and markers slide as the player turns. FullMapHud renders the world through an orthographic camera into a RenderTexture so the player sees actual terrain rather than a stylised abstraction. All three are UI Toolkit, built programmatically, and dock through the shared HUD slot registry so they coexist with everything else on screen.

Depended on by (2)

How it works

Concepts

A static registry for markers, a registered service for waypoints

Those are two different jobs, and they get two different mechanisms. Trackables are numerous, short-lived, and produced by MonoBehaviours whose OnEnable must not have to resolve a service first, so MinimapTrackableRegistry is static: Register and Unregister are both idempotent, Added and Removed events let a HUD cache per-marker presentation, and All hands back the snapshot the HUDs walk each frame.

The waypoint is a single piece of shared state with exactly one owner, so it is a real service. ZOAMinimapSceneInstaller registers a WaypointService against IWaypointService, gameplay code resolves it to set the player's destination, and both HUDs subscribe to WaypointChanged. Because it is resolvable rather than static, a project can swap in its own implementation for a mission system that owns waypoint selection.

Because the registry is static its state clears on domain reload but survives a scene load, and tests call Clear in teardown so claims do not leak between cases.

Category drives presentation, the marker can override it

MinimapTrackableCategory is the discriminator, and MinimapTrackable resolves each marker's appearance from it through two static tables. DefaultGlyphFor maps a category to the unicode glyph the HUD renders as a Label: an up triangle for the player, a filled dot for friendlies, a cross for enemies, a diamond for points of interest, a down triangle for waypoints, a star for objectives, and so on through vendors, save terminals, extraction zones and supply caches. DefaultTintFor maps each to a colour chosen for contrast against a dark map canvas.

An author overrides either per marker. Setting IconCharacter replaces the glyph; setting a tint with a non-zero alpha replaces the colour, and an alpha of zero is the sentinel meaning use the category default. The HUDs read ResolvedIcon and ResolvedTint, so the override logic lives in one place.

Two independent visibility flags decide which surfaces a marker reaches, ShowOnMinimap and ShowOnCompass, and MaxVisibleDistance hides a marker past a world-space range, with zero meaning always render. That last one matters in tactical-map scenes with a high marker count.

Runtime-spawned markers need Configure to re-fire

A producer that adds a MinimapTrackable at runtime, such as the AI bridge stamping a marker on every spawned agent, sees OnEnable fire and the registry register the marker before it gets a chance to configure the category. The HUDs cache each marker's resolved glyph and tint at registration time, so the marker keeps rendering with whatever defaults it had a moment earlier: an AI spawned at runtime shows up as a generic blue point-of-interest diamond instead of a red enemy cross.

Configure handles that. It snapshots the previous category, icon and tint, applies the new values, and if any of them would change the resolved appearance and the trackable is registered, it unregisters and re-registers to fire the Removed and Added pair the HUDs listen to. It does not churn those events when nothing visible changed, which would rebuild the label cache on every call.

Docking through the shared slot registry

Each HUD declares a preferred ZOAHudSlot and claims it through ZOAHudSlotRegistry in the common package: the minimap defaults to bottom-left, the compass to top-centre, and the full map to the stackable full-screen slot so it coexists with other full-screen overlays. If the claim comes back None because everything is taken, the widget falls back to its own inspector anchor through the same applier, so both paths dock identically.

Without it the minimap would land on top of the survival strip in a scene where another widget got there first. The registry logs a warning naming the existing claimants and reroutes instead of letting two widgets overlap.

Every HUD builds its own UIDocument programmatically and resolves the canonical panel settings from Resources at Awake, which is the per-widget pattern the rest of the presentation layer uses. Placing the minimap and compass on sibling GameObjects keeps their two documents from fighting.

Integration assemblies keep the layering honest

The AI package is gameplay and the minimap package is presentation, and neither depends on the other. The separate ZOA.Minimap.Integration assembly gets AI onto the minimap anyway: it references both, subscribes to the AI spawn service, and stamps an enemy-category trackable onto every spawned agent, giving any project a one-line opt-in without collapsing the layering.

MinimapPlayerDecorator solves the same shape of problem for the player rig. Under the spawn-request flow the rig does not exist at author time, so an author-time marker would land on the spawn anchor and the minimap would track where the player started rather than where the player is. The decorator resolves the player context from the registry at runtime, decorates whichever rig it surfaces, re-decorates on respawn, and no-ops when the rig already carries a trackable.

In the editor

Screens

Screenshot pending

/screenshots/minimap-and-compass-in-play.png

A gameplay frame with the circular minimap docked bottom-left and the compass strip across the top-centre, several markers of different categories visible on both so the glyph and tint scheme reads at a glance.

Minimap and compass strip in play

Screenshot pending

/screenshots/full-map-open.png

FullMapHud open over gameplay, showing the orthographic terrain render with trackable markers composited on top and the player marker at centre.

The full world map

Screenshot pending

/screenshots/waypoint-set.png

The same waypoint marker visible simultaneously on the minimap and the compass strip, with its label, after SetWaypoint was called.

An active waypoint on both surfaces

Screenshot pending

/screenshots/trackable-inspector.png

The marker component on a scene object with its category set, the icon and tint overrides left blank to show the defaults path, and the per-surface visibility flags visible.

MinimapTrackable in the Inspector

Setup

Workflow

  1. 01

    Install the service

    Drop a ZOAMinimapSceneInstaller onto the scene's GameManager root. It registers the waypoint service on Awake and is idempotent, so re-running it over a scene that already has one reuses the existing service.

  2. 02

    Add the HUD widgets on sibling objects

    Put MinimapHud on one child GameObject and CompassHud on another, siblings rather than nested, so their two UIDocuments do not fight. Each creates its own document with the canonical panel settings and claims its canonical slot. Add FullMapHud if the game wants a world map on a hotkey.

  3. 03

    Tag the player rig

    Give the player rig a Player-category trackable so the HUDs can resolve its position, or assign playerTransform explicitly on each HUD for split-screen pawns. Under a spawn-request flow use MinimapPlayerDecorator instead of an author-time component, so the marker lands on the runtime rig rather than the spawn anchor.

  4. 04

    Tag the world

    Put MinimapTrackable on the anchors worth showing, with the category that describes each: vendors, save terminals, hold zones, the boss arena, the extract pad. Set MaxVisibleDistance on dense scenes to keep distant clutter off the strip.

  5. 05

    Set waypoints from gameplay

    Resolve IWaypointService and call SetWaypoint with a world position and a label. Both HUDs pick the marker up automatically, and ClearWaypoint removes it. Setting a new waypoint replaces the old, so nothing accumulates.

Surface

Key types

MinimapTrackable

component

The marker component. Drop it on anything the HUDs should draw, and it auto-registers on enable and unregisters on disable. Position is read from the host transform each frame. Added under ZOA/Minimap/Minimap Trackable.

  • MinimapTrackableCategory Category { get; } string DisplayName { get; }
  • string ResolvedIcon { get; } Color ResolvedTint { get; }
  • bool ShowOnMinimap { get; } bool ShowOnCompass { get; }
  • float MaxVisibleDistance { get; } Vector3 Position { get; }
  • void Configure(MinimapTrackableCategory cat, string label, string icon = "", Color tint = default)
  • static string DefaultGlyphFor(MinimapTrackableCategory cat)
  • static Color DefaultTintFor(MinimapTrackableCategory cat)

MinimapTrackableCategory

enum

The discriminator that selects a marker's default glyph and tint, and reads as intent at the authoring site. A byte enum with Custom parked at 255 so new categories can be added in order.

  • Player, Friendly, Enemy, PointOfInterest
  • Waypoint, Objective, Vendor, SaveTerminal
  • Extract, Supply, Custom

MinimapTrackableRegistry

class

The static registry the HUDs query each frame. Register and Unregister are both idempotent, and the HUDs use Added and Removed to build and drop their cached marker labels.

  • static IReadOnlyList<MinimapTrackable> All { get; } static int Count { get; }
  • static void Register(MinimapTrackable trackable)
  • static void Unregister(MinimapTrackable trackable)
  • static bool IsRegistered(MinimapTrackable trackable)
  • static MinimapTrackable FindFirst(MinimapTrackableCategory category)
  • static event Action<MinimapTrackable> Added, Removed
  • static void Clear()

IWaypointService

interface

The player's active waypoint, of which there is at most one. Setting a new waypoint replaces the old rather than stacking, and HUDs hide their waypoint markers while ActiveWaypoint is null.

  • Vector3? ActiveWaypoint { get; } string ActiveWaypointLabel { get; }
  • event Action<Vector3?> WaypointChanged
  • void SetWaypoint(Vector3 worldPosition, string label = "")
  • void ClearWaypoint()

WaypointService

service

The default implementation. Pure C# with no MonoBehaviour, so the scene installer owns its lifetime. Replace-on-set semantics, and clearing an already-empty waypoint is a no-op that raises nothing.

  • raises WaypointChanged on every set, replace and clear
  • exceptions from subscribers are logged rather than allowed to escape

MinimapHud

component

Circular top-down minimap. Markers outside the configured world radius are clamped to the rim; the player marker holds the centre. Added under ZOA/Minimap/Minimap HUD.

  • serialized: playerTransform (optional; otherwise the first Player-category trackable)
  • serialized: preferredSlot (BottomLeft), anchor, anchorMargin
  • serialized: worldRadius, minimapPixelDiameter, rotateWithPlayer
  • serialized: backgroundColor, borderColor, markerFontSize
  • void SetAnchor(ZOAHudAnchor next)

CompassHud

component

Horizontal compass strip. Cardinal and intercardinal ticks plus per-trackable markers slide left and right with the player's bearing, across a configurable visible arc. Added under ZOA/Minimap/Compass HUD.

  • serialized: preferredSlot (TopCenter), anchor, anchorMargin
  • serialized: compassPixelWidth, visibleArcDegrees (default 180), stripHeight
  • serialized: backgroundColor, tickColor, markerFontSize
  • void SetAnchor(ZOAHudAnchor next)

FullMapHud

component

Full-screen world map on a hotkey. An orthographic camera looking straight down renders into a RenderTexture, with trackable markers composited on top, so the map shows real terrain rather than an abstraction. Added under ZOA/Minimap/Full Map HUD.

  • serialized: toggleKey (default M), startClosed, orthographicSize, cameraHeight
  • serialized: cullingMask, cameraBackground, borderColor, markerFontSize, panelTitle
  • bool IsOpen { get; } void Toggle() void SetOpen(bool open)
  • camera built lazily on first toggle; claims the stackable FullScreen slot

ZOAMinimapSceneInstaller

component

Per-package scene installer. Registers a WaypointService on Awake and unregisters it on destroy if the registry still points at its instance. The trackable registry is static and self-managing, so it needs no registration. Added under ZOA/Minimap/Minimap Scene Installer.

  • IWaypointService WaypointService { get; }
  • void EnsureRegistered()
  • serialized: dontDestroyOnLoad

AiMinimapBridge

component

Optional integration MonoBehaviour in the separate integration assembly. Subscribes to the AI spawn service and stamps an enemy-category trackable onto every spawned agent, so AI show up without per-prefab authoring and without either base package depending on the other. Added under ZOA/Minimap/AI Minimap Bridge.

  • idempotent: a prefab that already carries a trackable is left alone
  • retries service resolution until the AI scene installer has run

MinimapPlayerDecorator

component

Resolves the player context at runtime and decorates whichever rig it surfaces with a Player-category trackable, re-decorating on respawn. Use it where the spawn flow leaves no rig to author against. Added under ZOA/Minimap/Player Decorator.

  • serialized: playerLabel (default "Player")

Usage

Examples

Setting the active waypointcsharp
using UnityEngine;
using ZOA.Messaging;
using ZOA.Minimap.Unity;

public sealed class ObjectiveTracker : MonoBehaviour
{
    public void PointPlayerAt(Vector3 targetPosition)
    {
        if (FoundryServiceRegistry.TryResolve<IWaypointService>(out var waypoints))
        {
            // Replace-on-set: the player never accumulates a stack of
            // waypoints, and both HUDs update from WaypointChanged.
            waypoints.SetWaypoint(targetPosition, "Mission Objective");
        }
    }

    public void ObjectiveComplete()
    {
        if (FoundryServiceRegistry.TryResolve<IWaypointService>(out var waypoints))
            waypoints.ClearWaypoint();   // no-op when nothing was set
    }
}
TryResolve rather than Get: a scene without the installer should degrade to no waypoint, not throw.
Reacting to the active waypoint in a HUDcsharp
using System;
using UnityEngine;
using ZOA.Messaging;
using ZOA.Minimap.Unity;

public sealed class DistanceReadout : MonoBehaviour
{
    private IWaypointService _waypoints;

    private void OnEnable()
    {
        if (!FoundryServiceRegistry.TryResolve<IWaypointService>(out _waypoints))
            return;

        _waypoints.WaypointChanged += OnWaypointChanged;
        OnWaypointChanged(_waypoints.ActiveWaypoint);
    }

    private void OnDisable()
    {
        if (_waypoints != null) _waypoints.WaypointChanged -= OnWaypointChanged;
    }

    // The payload is null on clear, which is the signal to hide.
    private void OnWaypointChanged(Vector3? waypoint)
    {
        if (waypoint == null) { Hide(); return; }

        float metres = Vector3.Distance(transform.position, waypoint.Value);
        Show(_waypoints.ActiveWaypointLabel, metres);
    }

    private void Show(string label, float metres) { /* ... */ }
    private void Hide() { /* ... */ }
}
Stamping a marker onto a runtime-spawned objectcsharp
using UnityEngine;
using ZOA.Minimap.Unity;

public static class MarkerStamper
{
    public static void MarkAsEnemy(GameObject agent, string label)
    {
        var trackable = agent.GetComponent<MinimapTrackable>();
        if (trackable == null)
            trackable = agent.AddComponent<MinimapTrackable>();

        // AddComponent fires OnEnable, which registers the trackable
        // with its default category before this line runs. Configure
        // detects that the resolved glyph and tint changed and
        // re-fires Removed/Added so the HUDs rebuild their cached
        // labels; without it the agent renders as a generic
        // point-of-interest diamond rather than an enemy cross.
        trackable.Configure(MinimapTrackableCategory.Enemy, label);
    }
}
Configure is a no-op on the event front when nothing visible changed, so calling it defensively costs nothing.
Finding the player markercsharp
using UnityEngine;
using ZOA.Minimap.Unity;

// The HUDs resolve the player the same way when no explicit
// playerTransform is assigned: the first registered trackable in
// the Player category.
var player = MinimapTrackableRegistry.FindFirst(MinimapTrackableCategory.Player);
if (player != null)
    Debug.Log(player.DisplayName + " at " + player.Position);

// All is the snapshot the HUDs walk each frame.
Debug.Log(MinimapTrackableRegistry.Count + " trackables registered");

Read this

Notes and caveats

See also