Skip to content

Renderer Architecture

Combat text has a uniquely spiky performance profile: a boss fight can spawn dozens of numbers in a single frame, while most of the game they sit idle. MageIt Combat Text solves this by not forcing you into a single rendering strategy — instead it ships three renderers behind one API, so you pick the right cost/flexibility tradeoff per damage type.

Every renderer consumes the same FMageItCombatTextEvent struct through the same RenderDamage entry point. This means you can register a cheap Niagara renderer for high-frequency chip damage, and a fully animated UMG renderer for rare critical hits or boss telegraphs — all routed automatically by FGameplayTag.

RendererBackendBest forTradeoff
UMGPooled UUserWidgetsRich animation, accumulating damage, custom widget logicHigher per-instance cost; pool size caps concurrency
NiagaraGPU-driven SDF spritesHundreds of numbers on screen at onceLess per-number Blueprint customization at runtime
SlateDirect SLeafWidget paintLightweight built-in pop/float/fade text with zero widget allocationVisuals are code-driven, not designer-driven
  • ULocalPlayerSubsystem: By inheriting from ULocalPlayerSubsystem, the plugin avoids spawning invisible “Manager Actors” into your world. The subsystem exists solely in memory, tied directly to the player, and requires zero manual lifecycle management.
  • Lazy Renderer Instantiation: By default, renderers are only constructed the first time a FGameplayTag needs them (GetRenderer), and cached afterwards. A game that never triggers a Niagara-tagged event never pays for a Niagara renderer instance. Any renderer can opt out of this and warm up eagerly at subsystem startup instead, via bInitializeAtBeginPlay — useful for renderers whose first-use cost would otherwise cause a visible hitch mid-gameplay.
  • Widget Pooling: The UMG renderer pre-allocates a pool of widgets on Initialize and reuses them via ReturnWidgetToPool, avoiding per-hit CreateWidget allocations. An optional MaxPoolSize ceiling lets the pool grow under pressure instead of silently dropping events.
  • Batched Niagara Updates: The Niagara renderer doesn’t push one Niagara array update per damage event. It buffers events in PendingEventsBatch and flushes the whole batch once per Tick, keeping the number of SetNiagaraArrayVector4 calls constant regardless of how many hits land in a frame.
  • Cached Text Measurement: The Slate renderer caches FSlateFontMeasure results per unique rendered string in TextSizeCache, since damage numbers repeat heavily (lots of identical small integers) and text measurement is one of the pricier Slate calls.
  • Compile-Time Speed: The plugin’s C++ source code strictly adheres to the IWYU (Include What You Use) principle, utilizing forward declarations wherever possible, so adding this plugin to a C++ project will not bloat your compilation times.