Performance Best Practices

Following these practices keeps frame rate stable and CPU cost low in complex text animation scenarios.

1. Control Glyph and Layer Scale

Glyph Count

Each additional glyph adds layer count × non-empty channel count curve evaluations per frame. Recommendations:

  • Simple UI text (labels, buttons): < 30 glyphs
  • Title animations: < 80 glyphs
  • Long text (dialogs, descriptions): consider segmentation, < 150 glyphs per segment
  • Extreme scenarios (live danmaku, etc.): stay within 500 glyphs and use widget pooling

Layer Count

Each additional layer adds evaluations for the corresponding channels per glyph per frame. Recommendations:

  • Single-effect animations: 1 layer
  • Standard multi-effects: 2–3 layers (preset library defaults to ≤ 3)
  • Complex animations: ≤ 5 layers

Above 5 layers, consider merging effects into one layer (using Multiply modulation rather than separate layers).

2. Scheduling Early Exits

FBakedGlyph::Evaluate has three built-in skip mechanisms to maximize the share of “inactive” glyphs:

  1. bIsStatic — static glyphs directly return DefaultState (zero curve evaluation)
  2. Not started (LocalTime < 0) — returns the identity state without curve evaluation
  3. Finished (HasFinished) — evaluation skipped as well

Design suggestion: configure scheduling delays and skip flags so most glyphs are in a “not started / finished” state at any moment:

Layer->SetStartDelayAt(-1, 0.0f);   // base start delay
Layer->SetDelayAt(-1, 0.05f);       // per-glyph stagger → typewriter-style active window
Layer->SkipAt(-1);                  // skip all first
// restore the glyphs that need animation by classification
for (int32 i = 0; i < Layer->GetGlyphCount(); ++i)
{
    if (!Layer->IsWhitespaceAt(i))
    {
        Layer->SetSkipAnimationAt(i, false);
    }
}

3. Widget Pooling

Frequently creating/destroying UAnimatedTextBlock / UAnimatedRichTextBlock creates GC pressure and bake overhead. Consider a widget pool:

// Pooled usage pattern (with UAnimatedTextBlock as an example)
UAnimatedTextBlock* Widget = Pool->Dequeue();          // take from pool
Widget->Stop();                                        // reset state
Widget->SetText(FText::FromString(NewText));
Widget->SetBlueprintVariable(TEXT("Speed"), 2.0f);     // set parameters
Widget->Play();                                        // play

// when done
Widget->Stop();
Pool->Enqueue(Widget);                                 // return to pool

TIP

Size the pool for the peak number of simultaneously displayed animated texts; 32–64 is usually enough for UI scenarios. When widgets reuse the same Blueprint, each instance isolates parameters independently through ParameterOverrides without interference.

4. Precompilation & Fingerprint Cooldown

  1. All animation curves are baked on demand (initialization / text changes / Blueprint recompile) by FAnimationCompiler::BakeAllStages — zero bake overhead at runtime
  2. Dual fingerprints (TextFingerprint + BlueprintFingerprint) skip baking on cache hits
  3. Re-bake cooldown MinFramesBetweenRebakes = 3: do not modify parameters every frame — baking is throttled during parameter sweeps, but continuous modification still causes repeated bakes. One Play after batch parameter changes is more efficient
  4. Compile all UTextAnimationBlueprints before shipping to avoid first-run bake hitches

5. Rich Text Entry Strategy

Each <anim> tag region of UAnimatedRichTextBlock bakes and plays independently; simultaneous multi-entry playback grows linearly in cost:

  1. Centralize tag mappings in a UTextAnimationDataAsset
  2. Split long dialogs into short segments, using bSequentialPlayback (sequential playback) instead of playing everything at once
  3. Set bHideFirstFrame on entries not yet executed to avoid first-frame flashes during sequential playback
  4. Avoid many distinct animation Blueprint entries — reusing one Blueprint with entry-level parameter overrides is more memory-efficient than many separate Blueprints

6. Batching Recommendations

UAnimatedTextBlock inherits UMG’s standard text widgets; rendering is batched automatically by Slate:

  1. Text with the same font and material batches into few draw calls automatically
  2. Avoid modifying font properties during animation — font changes break batching
  3. For multi-font effects use Composite Font instead of stacking multiple widgets
  4. Per-glyph transforms (translation/scale/rotation) break batching — concentrate transform animations on a small number of widgets

7. Loops & Long Animations

  1. Prefer layer loops: SetPlayMode(direction, speed, NumberOfLoops)’s NumberOfLoops (0 = infinite) wraps LocalTime with fmod at evaluation, no need to bake very long animations
  2. Infinite-loop layers: with NumberOfLoops = 0, GetEffectiveDuration returns FLT_MAX (the stage never ends) — mind the overall duration semantics when mixing infinite-loop layers with finite ones
  3. Animation clipping (SetAnimationClipAt) trims unused front/back curves, reducing key counts

TIP

Performance optimization priority: reduce glyph count (SkipAt to skip whitespace) → reduce layer count → reduce non-empty channel count (delete unused tracks) → reuse Blueprints + entry-level parameters → batch widgets.

images/performance-comparison.png — Performance comparison chart: configurations on X axis (single-layer typewriter / three-layer title / rich-text multi-entry / pooled danmaku), per-frame evaluation count and frame time on Y axis, optimization points annotated per configuration