Skip to content

Pooled Prefab Views — Design

Goal: Move runtime-generated presentation objects onto authored prefabs, spawned through a shared pool under a tidy hierarchy root. First slice: floaters (also fixes the broken TMP drop-shadow). Same pattern then applies to world labels, health bars, and channel halos.

Why

TMP bakes per-vertex SDF padding from the material's outline/underlay state at mesh-generation time. The runtime floater set its text first (TMP auto-generates the mesh), then TextFx.ApplyReadable enabled the underlay keyword after — so the mesh had no padded vertices for the shadow until a later regen ("refresh"). A prefab whose material already has the underlay baked in generates correctly on frame one. Pooling additionally removes the per-spawn new GameObject / Destroy churn and organises the hierarchy.

Components

  • ViewPool (DI singleton, Presentation): generic component pool. Owns a —Views— root with per-type child containers (Floaters, later Labels/HealthBars/Halos).
  • T Rent<T>(T prefab, string container) — pop a free instance or Instantiate (stamping a PooledHandle that records the source prefab), reparent under the container, activate.
  • Return(Component) — deactivate, reparent to root, push back on the prefab's free stack.
  • PooledHandle: stamped on each instance so Return knows its prefab/pool.
  • Resources/Views/Floater.prefab + Lato World Text.mat: a TextMeshPro using a material variant of the Lato SDF material with outline+underlay pre-baked (values from GameConfig.text defaults). Mesh padding is correct from the first frame.
  • Floaters facade: holds the injected ViewPool + the cached prefab. Text(...) rents + Inits; a floater returns itself to the pool at end-of-life. Configure(cfg, pool) also pushes current readability values to the shared world-text material so live tuning still applies.
  • Floater: pooling-aware Init (resets fontStyle/timer/visibility each rent; no more per-instance ApplyReadable); on life-end calls Floaters.Return(this) instead of Destroy.

Wiring

  • RaidInstaller: Container.Bind<ViewPool>().AsSingle().
  • PlayerController.Construct gains the ViewPool; Init calls Floaters.Configure(_cfg.text, pool).
  • Prefab/material live in Assets/Resources/Views (loaded via Resources.Load, no scene edit, no serialized-ref scene churn).

Labels (done) — rent/return on vision

WorldLabel (enemy / boss / rival name tags + ground-loot labels) is now a pooled prefab sharing the floater's baked world-text material. Owners drive a LabelTag helper that rents a label only while the entity is in sight and returns it the moment it isn't — so out-of-vision entities hold no label at all (not merely hidden). The de-flickered Shown state prevents strobing at sight edges; the rich text is re-applied on rent and whenever it changes (e.g. a rival grabbing the key). Wired via Labels.Configure(pool) at player spawn. Validated: active labels == shown enemies + near/visible loot, no leak/duplication.

Shapes views (done) — health bars + channel halos

HealthBar (Shapes Line) and ChannelHalo (Shapes Disc) are now pooled prefabs too, rented via a generic Views facade (no text material needed). HealthBarView became a per-entity DRIVER that rents a pooled bar only while the bar should show (alive + seen + below threshold) and returns it on death/heal/fog. The rival's channel ring uses a HaloTag helper that rents on Set and returns on Hide; the now-unused authored ChannelHalo was stripped from Raider.prefab. Both views parent their Shapes to themselves so the pool's activate/deactivate hides them. The player's own interaction ring stays in PlayerVitalsView (a single HUD instance — not worth pooling).

All runtime presentation objects now spawn from the shared ViewPool under —Views—.

Verification

Play mode: floater drop-shadow renders fully on the first frame (no refresh needed); floaters recycle (pool reused, no per-spawn instantiate/leak); committed Lato font + underlay values untouched.