Concept: Styles, Formatting & the Widget Pool
Once an event survives the filter, three client-side systems turn it into pixels: the style registry picks how it looks, the formatter picks how the number reads, and the widget pool picks which widget shows it.
Style resolution
The registry evaluates every FCrimsonNumberPopStyleRule against the event's tags; among the rules whose FGameplayTagQuery matches, the highest Priority wins. No match falls back to DefaultConfig. This makes styles layerable: a broad NumberPop.Damage rule at priority 0 and a narrow NumberPop.Damage.Crit rule at priority 10 coexist without ordering tricks.
The winning FCrimsonNumberPopConfig supplies the rich-text FormatString with these tokens:
| Token | Replaced with |
|---|---|
{Value} | The formatted number |
{Prefix} | The event's PrefixText (e.g. - or +) |
{Custom} | The event's CustomText (e.g. Blocked!) |
{ValueStyle} / {PrefixStyle} | The config's rich-text style row names - wrap segments as <{ValueStyle}>{Value}</> |
Number formatting
The project default (UCrimsonNumberPopSettings::NumberFormat) applies unless the local player's NumberFormatOverride is 0 or greater (How-To: Filter Pops Per Player):
| Format | Example | Notes |
|---|---|---|
CommaSeparated | 1,234,567 | Locale-style grouping, no decimals |
Abbreviated | 1.2M, 3.4K, 5.6B, 7.8T | Compact suffix notation with one decimal |
Raw | 1234567 | No formatting |
The widget pool
Widgets are expensive to create and cheap to reuse, so the subsystem keeps two lists: InactivePool (ready) and ActivePool (on screen). At startup it async-loads the registry and widget class, then pre-warms InitialPoolSize widgets. Each displayed pop pulls from the inactive pool; when a pop finishes (OnPopFinished), the widget goes back.
Positioning: after AddToViewport, each widget is re-slotted into a point-anchored, auto-sized viewport slot with 0.5/0.5 alignment - so the widget is centered on its position and its render translation is the projected screen position. Every tick the subsystem projects the tracked world location to the screen, sets the render translation, applies the distance-scale curve when one is configured (final scale = BaseRenderScale x curve value; a curve value of 0 or less hides the pop), and hides widgets that go off-screen. (A default full-screen slot would make render translation a relative offset and pile pops up in one screen quadrant - the re-slot is what pins each pop to its world point.)
Border, Size Box, or Overlay roots do. A Canvas Panel root reports zero desired size and the pop collapses to nothing.| Guard | Setting | Behavior |
|---|---|---|
| Creation throttle | fixed (5 per frame) | When the inactive pool is empty, at most 5 new widgets are created per frame; excess requests wait in the queue for the next frame - no hitch on burst damage |
| Concurrent cap | MaxActivePops (default 40) | When the active count reaches the cap, the oldest active pop is force-finished (CancelPop) and recycled for the new one - AoE spam cannot grow the widget count. 0 = unlimited |
Widget lifecycle
Before calling StartPop, the subsystem stores the player's WidgetScale on the widget as BaseRenderScale (refined per frame by the distance-scale curve, How-To: Scale Pops by Camera Distance) and writes the style's AnimationName into ActiveAnimationName. The base StartPop sets the rich text, randomizes a velocity from the style's FCrimsonNumberPopPhysics (spread and boost ranges), and NativeTick integrates gravity, fades out over the last 20% of Duration, and broadcasts OnPopFinished - which returns the widget to the pool. A Blueprint override that calls the parent inherits all of that (How-To: Play a Custom Widget Animation).