How-To: Switch to the Batched Niagara Renderer

Rich text is not supported on this path
The Niagara renderer draws glyphs from a texture atlas, so the style's FormatString markup, {Prefix} and {Custom} are stripped. Take this path only when you genuinely need hundreds of simultaneous numbers and can give up formatting to get them.

When it is worth it

RendererCostRich textUse when
CrimsonCombatTextRenderer_Widget (default)One pooled UUserWidget per instanceYesAlmost always
CrimsonCombatTextRenderer_NiagaraOne Niagara system for everythingNoARPG / bullet-hell densities

This renderer needs two assets that cannot ship inside the plugin - a Niagara system and a glyph-atlas material. Everything below is the contract the C++ renderer pushes data against; build the assets to match and it works.

1. Glyph atlas texture

A single texture divided into a grid of equal cells, one glyph per cell, read in row-major order (left to right, top to bottom). Cell order must be exactly this:

text
index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
glyph: 0 1 2 3 4 5 6 7 8 9 . , - + K M B T ! ?

A 5x4 grid of 20 cells fits exactly. Characters outside this set are skipped rather than drawn as a placeholder, so unsupported text simply does not appear.

Use a signed-distance-field atlas
The scale curve can punch glyphs well above 1.0. An SDF atlas keeps them crisp at those sizes where a plain bitmap goes soft.
The order lives in one place in code
UCrimsonCombatTextRenderer_Niagara::GetGlyphAtlasOrder() returns this exact string. If you lay your atlas out differently, change that string to match - do not guess, the two must agree or every glyph renders as the wrong character.

2. Material

A translucent, unlit, camera-facing sprite material that:

  • samples the atlas using the particle's SubUV / sub-image index as the cell selector
  • multiplies by the particle color - the renderer supplies the per-instance tint here
  • multiplies alpha by the particle alpha - the renderer supplies the opacity curve output here
  • ignores depth, if you want text to read through geometry (typical for combat text)

3. Niagara system

Name it whatever you like; the renderer only cares about the parameter contract.

Required user parameters

All arrays are parallel and hold one entry per glyph, not per number - a 1234 contributes four entries. Index them by particle ID.

ParameterTypeMeaning
GlyphPositionsPosition arrayWorld position of this glyph, motion already applied
GlyphIndicesint32 arrayAtlas cell index (see the order above)
GlyphColorsColor arrayPer-instance tint, repeated for each of its glyphs
GlyphScalesfloat arrayFinal scale, repeated for each of its glyphs
GlyphAlphasfloat array0..1 opacity, repeated for each of its glyphs
GlyphCountint32Number of valid entries; the arrays may be longer

Emitter behaviour

Spawn exactly GlyphCount particles, or keep a fixed pool and cull above GlyphCount. Then per particle, with i as the particle index:

  • position = GlyphPositions[i]
  • sub-image index = GlyphIndices[i]
  • color = GlyphColors[i], alpha = GlyphAlphas[i]
  • size = base sprite size * GlyphScales[i]
Do not animate anything in Niagara
No motion, no fade, no lifetime. The subsystem owns all of that and rewrites every array each frame - particles here are pure presentation. A particle that ages out on its own will flicker. The system is spawned once with bAutoDestroy = false and lives for the whole session.

4. Wire it up

  1. Create a Blueprint subclass of CrimsonCombatTextRenderer_Niagara.
  2. Assign your system to its NiagaraSystem property.
  3. Tune LayoutUnitsToWorld and GlyphAdvance until motion and spacing read correctly at your typical combat distance.
  4. Point Project Settings > Crimson > Crimson Combat Text > Renderer Class at that Blueprint.
Project Settings > Crimson > Crimson Combat Text with Renderer Class set to a Niagara renderer Blueprint.
Misconfiguration falls back, it does not go blank
With no system assigned, or if the system fails to spawn, InitializeRenderer returns false and the subsystem uses the widget renderer instead, with a warning in the log. You get working combat text and a clear message, never an empty screen.

Known limitations

LimitationDetail
No rich textSupportsRichText() returns false. Markup, {Prefix} and {Custom} are stripped; the subsystem warns once when this renderer is active.
Motion is projected, not nativeMotion is authored in screen-space layout units and projected onto the camera's right/up axes by LayoutUnitsToWorld. It will not track a screen-space path exactly under strong perspective.
MaxGlyphsPerFrame truncatesAbove the cap, remaining glyphs are dropped for that frame and a warning is logged. Truncation is never silent.

See also

  • Concept: Style Resolution, Motion & Rendering - where the renderer sits in the pipeline
  • How-To: Author Motion Presets - motion is renderer-agnostic and authored the same way either path