How-To: Layered Pool Bars

Goal: draw a combat resource whose layers all share one track - a health fill, a shield film over it, a temporary barrier appended past the maximum, reserved capacity locked off at the end, and a trail band that reads both damage and healing.

Prerequisites
None beyond a widget you can add an Image to. UCrimsonPoolBarWidget takes plain numbers and knows nothing about the ability system - see CrimsonCore -> Vitals: Shield, Barrier & Reservation for the GAS-driven half. Tick Show Plugin Content in the Content Browser settings to see the shipped assets.

Why one material and not stacked progress bars

Every layer here shares a track, a chamfered shape and a fill edge. Split across widgets, each one re-derives the same reservation remap and they drift apart by a pixel; each needs its own copy of the shape mask, and their antialiased edges composite into a bright seam; and each distinct material breaks the Slate batch, which a HUD carrying thirty nameplates pays for thirty times over.

So everything on the track is one Image driven by one dynamic material instance. Anything not on the track - readouts, status icons, a low-health vignette, floating combat numbers - stays a separate widget, because none of it shares the geometry.

1. Create the widget

Make a Widget Blueprint parented to CrimsonPoolBarWidget and give it one Image named exactly Image_Bar. That name is a BindWidgetOptional, so the widget still runs without it - it just does not draw.

Reference implementation
Open the shipped W_CrimsonPoolBar (/CrimsonUI/UI/Foundation/Widgets/). Its whole tree is SizeBox_Root -> Image_Bar, with the brush set to MI_UI_CrimsonPoolBar_Nameplate and an explicit brush Image Size. Duplicate it as your starting point.
Give the brush a size
A UI material has no intrinsic size. An Image whose brush Image Size is left at zero collapses to nothing and the bar is invisible with no error anywhere. Set the size on the brush, or wrap the Image in a SizeBox.
Verify
Drop the widget on a HUD layout and PIE. You should see the empty track with its rim and chamfered corners, even before any values are pushed.

2. Point it at a material

Set Style -> Bar Material to one of the shipped instances. If you leave it unset the widget falls back to whatever material is already on Image_Bar's brush, so setting the brush in the designer alone is enough - but naming it in Style is clearer and survives a brush change.

InstanceUse forNotes
MI_UI_CrimsonPoolBar_PlayerThe player's HUD barTall, segment ticks, full rim.
MI_UI_CrimsonPoolBar_NameplateFloating enemy platesHairline rim, no ticks. Four decorative features are switched OFF as static switches, so they compile out of the shader: 143 -> 106 pixel instructions.
MI_UI_CrimsonPoolBar_BossBoss / target barsWide, heavily segmented so a long drain still reads.

All three parent M_UI_CrimsonPoolBar (/CrimsonUI/UI/Foundation/Materials/), which shares its pixel-accurate chamfer with the M_UI_ChamferBox frame master through Functions/MF_UI_ChamferBoxSDF. That master and the function library beneath it (MF_UI_PixelSpace, MF_UI_ChamferSDF, MF_UI_EdgeBand, MF_UI_TopShade, MF_UI_SoftShadow, MF_UI_AccentBar, MF_UI_LinearSweep) are the frame material family every Crimson panel, row plate, hairline and sweep instances; see the README's Frame materials section for the parameter table.

A missing material fails silently-looking, not silently
With no material at all the widget logs a warning through LogCrimsonUI and draws nothing. The failure that used to be silent - a static material instance on the brush with Style.BarMaterial unset - is now handled by the fallback: the widget builds its dynamic instance from the brush. Without that, every parameter write goes to an instance that was never created and the bar looks correct while never moving.
Verify
In PIE, the brush's resource object on the live widget is a MaterialInstanceDynamic, not a MaterialInstanceConstant. If it is still Constant, no parameter will ever apply.

3. Push values

Fill an FCrimsonPoolSnapshot and hand it over. Use Set Pool State for a change the player should see animate, and Set Pool State Immediate on bind, on respawn, and anywhere a change is not a hit or a heal worth showing.

FieldMeaning
HealthCurrent health.
MaxHealthUntaxed total. This is the whole width of the track, reserved capacity included.
ReservedSlice of MaxHealth that cannot be filled. Drawn as a hatched dead zone at the end.
ShieldAbsorb pool. Drawn as a translucent film across the whole track.
MaxShieldShield capacity. The film is Shield / MaxShield, not a slice of MaxHealth.
OverhealthTemporary barrier. May exceed MaxHealth; drawn past the 100% mark.

Event Construct -> Make Crimson Pool Snapshot (set Health, MaxHealth, MaxShield, Shield) -> Set Pool State Immediate (Target: self).

For later changes call Set Pool State instead, so the trail band and the eased rise both play.

Verify
Move the values and the fill follows. Drop Health and the fill snaps down with a dark trail closing behind it; raise it and the fill eases up with a green band ahead of it.

4. Read the derived fractions

Compute Fractions is a public static and is the normalization specification. Nothing else may re-derive these numbers - the material consumes them already normalized, which is why the numeric readout and the pixels cannot disagree.

FractionMeaning
MainFracSeam between the main pool and the barrier tail. 1 when there is no barrier.
HealthFracRight edge of the health fill, as a share of the whole track.
GhostFracFar edge of the trail band. Never below the drawn fill.
ShieldFracShield / MaxShield, 0..1, across the whole track.
ReservedStartLeft edge of the reserved dead zone.
HealthPercentHealth over EFFECTIVE max. Drives the low-life treatment and the readout.
Target vs drawn
Get Pool Fractions returns where the bar is heading. While a gain eases in, the material is showing something slightly behind it - read Get Display Health Frac and Get Display Shield Frac for what is actually on screen.

5. Drive the readouts

On Pool Visual fires on every change with the raw snapshot, the derived fractions and a bIsLow flag. Use it for everything a material cannot draw: the numeric text, a barrier badge, a low-health warning.

Show the effective max, not MaxHealth
A character with a quarter of its pool reserved is at FULL health at the effective number. A readout of Health / MaxHealth would show it as permanently wounded. Use MaxHealth - Reserved.
Verify
Reserve part of the pool and heal to full. The bar reads full, the dead zone is hatched, and the text shows the same number twice.

6. Tune the motion

Everything below lives on Style (FCrimsonPoolBarStyle) and can be set per widget or swapped wholesale at runtime with Set Pool Bar Style.

PropertyDefaultEffect
GhostDelay0.15Seconds the trail holds at the old value before closing.
GhostSpeed3.0How fast the trail closes on the fill.
FillRiseSpeed4.0How fast a fill eases UPWARD. Drops are always instant.
FlashDuration0.12Seconds the white damage flash takes to decay.
LowThreshold0.30Health percent below which the low-life treatment engages.
CompressionStrength0.75How hard the main pool is squeezed to make room for overhealth.
MinMainFraction0.55Floor on the main pool's share of the track.
Segments0Tick count across the unreserved track. 0 disables.
Drops are instant on purpose
A hit has to land on the frame it happens, and the trail behind it is what carries the size of the loss. Easing a drop makes damage feel soft and delays the information the player most needs. Gains have the opposite problem - a shield recharging in periodic steps looks like a staircase unless it is smoothed.

The trail band reads both directions

There is one band, not two. Falling, it spans the fill out to where the fill was, in GhostColor. Rising, it spans the fill out to where the fill is going, in HealColor. Identical geometry and the same material layer - only the far edge and the tint change, so a gain reads with the same weight a loss does.

SituationBand
Damage landsRed, fill -> previous drawn position.
Healing with no damage pendingGreen, fill -> incoming target.
Healing while a loss is still closing ABOVE the new targetStays red - the unclosed loss is not erased.
Damage lands mid-healRed, anchored to what was on SCREEN, never to the undrawn heal target.
Anchoring matters
Anchoring the trail to the previous target rather than the previous drawn value overstates a hit taken during a heal - the band pops out to a number the player never saw.

Compression

Overhealth is unbounded, so appending it linearly would erase the health readout the moment a build stacks barriers. The main pool is squeezed logarithmically instead, and floored:

text
MainFrac = clamp( 1 / (1 + log2(1 + Overhealth / MaxHealth) * CompressionStrength),
MinMainFraction, 1 )
Overhealth 0% -> MainFrac 1.000 barrier takes 0%
Overhealth 50% -> MainFrac 0.719 barrier takes 28%
Overhealth 100% -> MainFrac 0.571 barrier takes 43%
Overhealth 400% -> MainFrac 0.550 barrier takes 45% (floored)

Performance

Node count is a compile-time figure and is not what the GPU runs. Material functions and reroute nodes are inlined and cost nothing; they buy readability. Static switches are the lever that actually pays, because the unused branch is compiled out of the instance rather than multiplied by a zero parameter every pixel.

MaterialPixel instructionsTexture samples
M_UI_CrimsonPoolBar (all features)1430
MI_UI_CrimsonPoolBar_Nameplate (4 switches off)1060
M_ProgressBar_Segmented (for comparison)1282
Measure, do not guess
unreal.MaterialEditingLibrary.get_statistics(asset) reports these, and it works on material instances too - so a per-instance static-switch saving is directly measurable.

Motion runs on a timer, not NativeTick

The trail, the flash and the eased fills all advance on a self-stopping repeating timer. UUserWidget suppresses ticking when nothing needs it, and a screen full of nameplates should cost nothing while everyone is at full health. The timer starts when something is in flight and stops itself the moment everything has settled.

See also

  • How-To: UI Extension Points - registering a bar at a HUD slot.
  • CrimsonCore -> Vitals: Shield, Barrier & Reservation - the GAS-driven bar.
  • CrimsonCore -> Nameplates - the floating enemy plate that embeds one of these.
  • API Reference -> Layered pool bars