Attribute UI without Tick
A health bar that reads the attribute every frame works, until there are 24 characters on screen with three bars each. Attributes change at event rate. The UI should update at event rate too.
The cost of polling
A widget that reads an attribute in Tick, or through a property binding, runs every frame whether the value changed or not. One bar is cheap. Nameplates on every character, each with health, shield and a cast bar, add up to real game-thread time, spent reading values that change a few times a second.
We measured the same pattern for ability state in C++ helper functions: per-frame queries for data that changes at event rate cost 1.9 ms per frame across 24 characters, and 0.07 ms once they were event-driven.
The event you want
Every ASC has a change delegate per attribute:
ASC->GetGameplayAttributeValueChangeDelegate(UMyAttributeSet::GetHealthAttribute())
.AddUObject(this, &UMyHealthBar::OnHealthChanged);
void UMyHealthBar::OnHealthChanged(const FOnAttributeChangeData& Data)
{
SetPercent(Data.NewValue / MaxHealth);
}It fires on the server when an effect changes the value, and on clients when the attribute replicates or a predicted change applies. Bind once, unbind when the widget goes away.
A Blueprint node for it
The free GAS Helpers add an async node, Watch Attributes, that wraps the delegate:
- Takes several attributes at once (health and max health together).
- Fires once immediately with the current values, so the bar is right on the first frame.
- Finds the ASC through the PlayerState, controller or owner, so you can pass the pawn.
- Cancel unbinds every delegate. Call it in the widget's Destruct.
void URSAsyncWatchAttribute::Activate()
{
UAbilitySystemComponent* ASC = WeakASC.Get();
if (!ASC)
{
Cancel();
return;
}
for (const FGameplayAttribute& Attribute : Watched)
{
if (!Attribute.IsValid())
{
continue;
}
Bindings.Emplace(Attribute, ASC->GetGameplayAttributeValueChangeDelegate(Attribute)
.AddUObject(this, &URSAsyncWatchAttribute::HandleChange));
if (bFireInitial)
{
const float Value = ASC->GetNumericAttribute(Attribute);
OnChanged.Broadcast(Attribute, Value, Value);
}
}
}The engine also ships a Wait for Attribute Changed async node for a single attribute. Use whichever fits; the point is the event, not the node.
In the widget Blueprint: on Construct, call Watch Attributes with the owning pawn and [Health, MaxHealth], store the returned task, and on On Changed update the bar. On Destruct, call Cancel on the stored task.
The client timing trap
With the ASC on the PlayerState, a client's HUD can construct before the PlayerState has replicated. The ASC is not there yet, the watcher finds nothing and cancels, and the bar never updates.
Do not fix this with a retry timer. Create or initialize the HUD from the point where the ASC is known to be ready: right after InitAbilityActorInfo in OnRep_PlayerState on the client and in PossessedBy on the server. The PlayerState setup shows both. A simple pattern is a multicast delegate on the character, OnAbilitySystemReady, that the HUD binds to.
Nameplates for other players
Simulated proxies (other players' characters) receive attributes only if the attribute set replicates to them, which it does with the default COND_None condition. If you changed attribute replication to owner-only to save bandwidth, nameplates for other players will never update. Keep the attributes other players need visible, and make the rest owner-only.
Common mistakes
- Binding in Construct with no matching unbind. A respawned pawn leaves the old widget's delegate behind, and it fires into a destroyed widget.
- Reading
MaxHealthonce at construct time. Buffs change it. Watch it too. - Using a property binding in UMG "just for this one". Property bindings run every frame.
Get the code
URSAsyncWatchAttribute is in GAS Helpers v0.1.0. See the implementation, free under the MIT license.
UI cost is one line in a performance budget. If frame time is slipping and you do not know where, a Diagnostic profiles it in Unreal Insights and gives you the fix list.