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:

TokenReplaced 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):

FormatExampleNotes
CommaSeparated1,234,567Locale-style grouping, no decimals
Abbreviated1.2M, 3.4K, 5.6B, 7.8TCompact suffix notation with one decimal
Raw1234567No 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.)

Auto-size needs a desired size
Because the slot auto-sizes to the widget's desired size, the pop Blueprint's root must report one - Border, Size Box, or Overlay roots do. A Canvas Panel root reports zero desired size and the pop collapses to nothing.
GuardSettingBehavior
Creation throttlefixed (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 capMaxActivePops (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).

Physics presets
Good starting values - Normal hit: Duration 1.0, Spread -50..50, Boost -300..-400, Gravity 900 (arcs down). Crit: Duration 2.0, Spread -20..20, Boost -600..-700, Gravity 200 (shoots up, floats). Heal: Duration 1.5, Spread -10..10, Boost -200..-250, Gravity -50 (drifts up). Negative boost is up; positive gravity pulls down.