Skip to content

Nameplates, Floating Health Bars, and Damage Numbers

Floating combat UI should read replicated or local gameplay state and draw it near the right actor. The widget is never the truth, and one reused widget must never keep another actor's data.

At a glance

Use this for: enemy health bars, player nameplates, team labels, objective labels, hit numbers, healing numbers - Main choices: Widget Components, viewport-projected widgets, pooled popup widgets, materials/Niagara for non-text feedback - State owners: HealthComponent or pawn for body health, PlayerState for public player identity/score/team, GameState for shared objective state - Official docs: Widget Components, Game Mode and Game State, and Replicate Actor Properties

The one-minute version

  • A floating health bar should read a health value owned by the actor or its HealthComponent. It should not store the only health value in the widget.
  • A player nameplate should usually read public identity/team data from PlayerState, because other clients can read PlayerStates and pawns can be replaced.
  • Do not replicate widgets or damage-number widgets. Replicate state or send an approved cosmetic event; each client draws its own local presentation.
  • Widget Components are good for actor-attached bars. Viewport-projected widgets are good when you want screen-space readability and custom overlap rules.
  • Pool fast popup widgets or use Niagara/materials for repeated damage numbers. Creating dozens of widgets during rapid fire is avoidable churn.
  • Every floating widget needs a source reference, refresh path, hide/show rule, and cleanup/rebind rule.

Pick the presentation shape

Shape Use when Watch out
Widget Component UI should live on an actor in the world, such as an enemy bar or in-world label. Scale, occlusion, facing, redraw, and actor lifetime matter.
Screen-space Widget Component Label should follow an actor but stay screen-readable. It may ignore walls/occlusion expectations.
Viewport-projected widget HUD should position markers over targets with custom sorting/off-screen rules. You own projection, clamping, overlap, and split-screen owner rules.
Pooled viewport popup Damage numbers should animate briefly and cheaply. Needs source position, lifetime, reuse reset, and visibility cleanup.
Niagara/material feedback The feedback is mostly visual shape/color, not UMG layout/text. It is not a button or rich widget.

Start with Widget Components for a few enemy bars. Move to projection/pooling when you need many labels, sophisticated hiding, or screen-space stacking rules.

What state each widget should read

Use the state owner that matches the thing being displayed:

Display Better source Why
Enemy current health Enemy HealthComponent or pawn/actor health variable. It describes the current body.
Player display name PlayerState. Other clients can read it; it survives pawn replacement.
Team color PlayerState or replicated team component/state. It is public per-player or per-actor classification.
Objective label/progress Objective actor/component or GameState, depending on whether it is local to one world actor or match-wide. The widget should read the gameplay owner.
Floating damage number Damage event data, replicated health delta, or approved cosmetic event. It is a transient presentation of a real hit/result.
Scoreboard row PlayerState and GameState. Widgets display public replicated state, not own it.

The widget should receive a reference or compact data:

WBP_Nameplate.Setup(SourceActor, SourcePlayerState, HealthComponent)
WBP_Nameplate.RefreshName(SourcePlayerState.PlayerName)
WBP_Nameplate.RefreshTeam(SourcePlayerState.TeamId)
WBP_Nameplate.RefreshHealth(CurrentHealth, MaxHealth)

Do not make WBP_Nameplate call Get Player Character(0) to find the target. That finds the local player's body, not the actor wearing this nameplate.

Floating health bars

Actor-attached setup:

BP_Enemy BeginPlay
-> Get WidgetComponent
-> Get User Widget Object
-> Cast to WBP_FloatingHealthBar
-> WBP_FloatingHealthBar.Setup(HealthComponent)
-> bind HealthComponent.OnHealthChanged
-> initial RefreshHealth(CurrentHealth, MaxHealth)

Health update:

HealthComponent changes health
-> OnHealthChanged(Current, Max, Delta)
-> widget percent = Current / Max clamped 0..1
-> optional show for 2 seconds if damaged
-> hide if dead or Current == Max and project wants clean UI

If the actor respawns as a new pawn, the old widget belongs to the old actor. The HUD/controller or new actor setup must bind the new widget to the new HealthComponent.

Nameplates

Nameplates need identity and visibility rules:

Nameplate refresh:
    DisplayName = PlayerState.PlayerName or actor display name
    TeamColor = team lookup from PlayerState.TeamId
    bVisible = target alive and in range and not hidden by local rule

Common hiding rules:

  • hide enemies beyond readable distance;
  • hide teammates only when far away or behind the camera;
  • hide dead/spectating players unless the mode wants corpse labels;
  • hide targets blocked by line of sight if wallhacks are not desired;
  • hide owner-only private labels from other clients.

Facing is local presentation:

Local client:
    nameplate world widget rotates toward this client's camera

The server should not rotate a replicated nameplate toward one client's camera, because another client needs a different facing direction.

Damage numbers

Damage numbers are transient UI. Do not make them the proof that damage happened; health/damage state is the proof.

Beginner-safe flow:

Server confirms hit
-> target health changes
-> server may send owning/relevant clients approved hit feedback
-> each client spawns or reuses a local damage number widget at hit/target location
-> widget animates upward/fades
-> widget returns to pool or removes itself

For single-player, the same flow can be local:

HealthComponent accepted 23 damage
-> DamageNumberPresenter.ShowNumber(TargetActor, 23, DamageType)

Pool if numbers happen often:

DamageNumberPool:
    inactive widgets
    active widgets with target/world location and remaining time

ShowNumber:
    take inactive widget or create if pool not full
    reset text/color/animation/source position
    activate

OnFinished:
    hide and return to inactive pool

If numbers are mostly motion/particles instead of rich text layout, Niagara can be a better presentation tool.

Projection and screen stacking

Viewport-projected labels use a world point and place a HUD widget at the projected screen position:

For each visible tracked target:
    WorldLocation = target location + label offset
    ProjectWorldLocationToWidgetPosition(owning PlayerController, WorldLocation)
    if projection succeeds and target passes visibility rules:
        place/update label widget
    else:
        hide or show off-screen indicator

This gives you custom rules Widget Components do not solve:

  • clamp indicators to screen edges;
  • stack overlapping labels;
  • sort by distance or target priority;
  • hide labels behind walls with a separate trace;
  • handle split-screen with each local player's viewport.

Use projection when those rules matter. Use Widget Components when actor-local setup is enough.

Multiplayer boundary

Widgets are local. State is replicated:

Server:
    HealthComponent.CurrentHealth RepNotify on replicated actor
    PlayerState.DisplayName / TeamId replicated
    GameState.ObjectiveState replicated

Client:
    OnRep / local apply helper updates nameplate or floating bar
    local presenter shows damage numbers based on approved hit/result data

Owner-only information, such as "this enemy is the target of my quest tracker" or "my stealth scan revealed this actor," should not be placed in public PlayerState or GameState unless everyone should know it. Keep private UI data owner-only or local.

When it fails, and what failure looks like

Symptom Likely cause
Every bar shows the local player's health Widget reads Get Player Character(0) instead of its source actor/component.
Names reset on death Name stored only on pawn, not PlayerState or another persistent owner.
Damage numbers create hitches New widgets are created for every hit with no pooling or cap.
Widget shows old actor data Reused widget did not clear/reassign source reference during setup.
Client sees different team color than others Client changed local UI/state without server-owned replicated team data.
Labels show through walls unexpectedly Screen-space/projection path has no occlusion test.
World labels vanish behind nearby geometry World-space Widget Component is being correctly occluded; use screen-space/projection if that is not desired.
Split-screen label is in the wrong place Projection or owner player uses player index 0 instead of the owning local player.

Print the widget instance name, source actor, PlayerState, HealthComponent, and net mode. Wrong-source bugs are the common case.

What floating UI does not do

  • It does not own health, score, team, or objective state.
  • It does not replicate itself to other machines.
  • It does not guarantee a damage event happened.
  • It does not validate whether a target should be visible to this player.
  • It does not automatically unbind from an old source when actors respawn or widgets are pooled.
  • It does not make Widget Components free to tick/redraw on every actor.

Lookalikes - which one do I want?

Tool Use when Watch out
Widget Component One actor owns an attached label/bar. Source setup and redraw cost still matter.
Viewport-projected marker HUD needs screen-space sorting, clamping, overlap handling, or off-screen arrows. You own projection and visibility tests.
RepNotify Replicated state should refresh local presentation as it arrives. It is state arrival, not a guaranteed event log for every damage tick.
PlayerState Public per-player name/team/score should be visible to clients and survive respawn. Not private local UI.
GameState Shared objective or team score should be visible to all clients. Not one actor's private body health.
Niagara/material indicator Feedback can be visual effect rather than rich UI. Not suitable for normal UMG text/buttons.

Rule of thumb: replicate compact gameplay facts, then draw local floating UI from the actor, PlayerState, or GameState that owns those facts.

Going deeper