HUD State from Replicated and Local Data¶
A HUD is a local view assembled from several owners: local input state, the currently possessed pawn, public PlayerState, public GameState, and replicated actor/component state. Widgets do not replicate; the state they read does.
At a glance
Use this for: health/ammo HUDs, objective panels, scoreboards, ready
screens, prompts, cooldowns, owner-only indicators -
Main owner: local PlayerController/HUD path creates widgets; gameplay
objects own state -
Main rule: local UI combines sources, but authoritative gameplay changes
still happen on the server -
Official docs: User Interfaces and HUDs,
Game Mode and Game State,
and Replicate Actor Properties
The one-minute version¶
- The HUD widget exists locally for one player. Creating it on one machine does not create it on another.
- Put the HUD creation/rebinding path in the PlayerController or HUD setup, not only in a pawn that can die and be replaced.
- Read body-specific state from the current pawn/character or its components: health, body ammo, movement state, interact target.
- Read public per-player state from PlayerState: name, team, score, ready, deaths, public loadout.
- Read public match/objective state from GameState or replicated objective actors/components.
- Use RepNotify or local dispatchers to push changes into widgets. Use an initial refresh after binding because old events are not replayed.
The local HUD is an assembler¶
The HUD should not be the owner of every value it displays. It assembles values from the right gameplay owners:
Local PlayerController / HUD:
creates WBP_HUD
stores HUDWidget
knows current local player context
rebinds when possessed pawn changes
Pawn / Character / components:
health, ammo on current body, interact target, movement mode
PlayerState:
public score, team, display name, ready state, deaths
GameState:
match phase, timer, team score, objective progress, wave number
Local-only systems:
input device, settings, local prompts, cooldown prediction, menu focus
This split prevents the two classic bugs:
"My HUD disappeared/reset when I died."
-> HUD was owned only by the pawn.
"Clients cannot see the scoreboard data."
-> public score/team was stored in local UI or PlayerController.
What belongs where¶
| HUD value | Better source | Notes |
|---|---|---|
| Current body health | Current pawn/HealthComponent. | Rebind after respawn. |
| Current body ammo | Weapon/component on current pawn, or replicated inventory/equipment state. | Decide whether it is public, owner-only, or local presentation. |
| Crosshair spread | Local weapon/movement/prediction state. | Usually owner-only presentation. |
| Interact prompt | Local trace + target prompt contract. | Server still validates final interaction. |
| Player score | PlayerState. | Public and survives pawn replacement. |
| Team score | GameState. | Shared match state. |
| Match phase/timer | GameState. | Clients read replicated public state. |
| Scoreboard rows | GameState PlayerArray of PlayerStates. |
Handle late arrival and join/leave. |
| Private inventory details | Owning pawn/component/client data, often owner-only replication. | Do not expose private data in public GameState. |
| Graphics/input settings | GameInstance/SaveGame/local settings. | Local only, not replicated match truth. |
If the value should be visible to everyone, avoid PlayerController and widgets. If it should survive death, avoid only storing it on the pawn.
Creation and initial binding¶
Create the main HUD once from the local player path:
PlayerController BeginPlay
-> if Is Local Controller
-> Create Widget WBP_HUD (Owning Player = Self)
-> Store HUDWidget
-> Add to Viewport
-> BindToCurrentPawn()
-> BindToPlayerState()
-> BindToGameState()
-> InitialRefreshAll()
Each bind should tolerate None because replicated objects can arrive after
the widget exists:
BindToPlayerState:
PlayerState = Get Player State
if PlayerState valid:
bind PlayerState.OnPublicStateChanged
HUDWidget.UpdatePlayerState(PlayerState values)
else:
clear player-state UI or retry when state is assigned
Do not assume BeginPlay order means every replicated object is already ready
on every client.
Rebinding after respawn¶
The current pawn is replaceable. The HUD should be stable across pawn replacement:
OnPossessedPawnChanged or controller possession path:
OldPawn = CachedPawn
if OldPawn valid:
unbind from old HealthComponent / weapon / inventory events
CachedPawn = Get Pawn
if CachedPawn valid:
bind to new HealthComponent / weapon / inventory events
initial refresh from new body values
else:
show dead/spectating/respawn UI as appropriate
This is why the PlayerController/HUD path is usually better than creating the only HUD inside the pawn's BeginPlay.
RepNotify plus local dispatchers¶
Replicated state arrives on local actor copies. Widgets update from that local copy:
Server:
GameState.MatchPhase = InProgress (RepNotify)
Client:
OnRep_MatchPhase on local GameState copy
-> GameState calls OnMatchStateChanged dispatcher locally
-> HUDWidget.UpdateMatchPhase(GameState.MatchPhase)
For pawn health:
Server:
HealthComponent.CurrentHealth changes
Client:
OnRep_CurrentHealth or local OnHealthChanged apply helper
-> HUDWidget.UpdateHealth(CurrentHealth, MaxHealth)
Use one presentation helper so listen servers and standalone play also refresh through the same visible path.
Scoreboard rows¶
Scoreboards combine GameState and PlayerState:
Open scoreboard:
Get GameState
Get PlayerArray
ForEach PlayerState
create/update one row per PlayerState
PlayerState Score/Team/Name RepNotify:
row for that PlayerState refreshes
Player joins/leaves:
rebuild or diff rows from PlayerArray
Do not identify players only by array index forever. Use PlayerState references or stable IDs/names appropriate for your project, because join/leave order can change.
Owner-only and local-only data¶
Not every HUD value is public. Examples:
- local interact prompt;
- "reload now" warning;
- private quest clue;
- inventory contents in games where inventory is not public;
- local input-device icon;
- local predicted cooldown fill;
- stealth visibility information only this player has earned.
Owner-only data should travel through owner-only replicated variables/RPCs or local systems, not public GameState. Local-only data should not be replicated at all unless other players truly need it.
Split-screen¶
Split-screen makes "local UI" plural. Each local player needs its own owning player context:
For each local PlayerController:
Create Widget with Owning Player = that controller
Add to Player Screen when appropriate
use that controller for projection/input prompts
Avoid hard-coding player index 0 in widgets, projection helpers, or prompt
systems if split-screen is possible.
When it fails, and what failure looks like¶
| Symptom | Likely cause |
|---|---|
| HUD works for host only | Widget creation/update path runs on server or host but not each local owning client. |
| Health bar breaks after respawn | HUD stayed bound to old pawn/HealthComponent. |
| Scoreboard rows are empty on clients | Score stored in GameMode, PlayerController, or widget instead of PlayerState/GameState. |
| UI flashes defaults at match start | Widget created before replicated state arrived and skipped initial refresh/retry. |
| Other players see private data | Owner-only/local-only data was replicated publicly through GameState or PlayerState. |
| Client changes score locally then it snaps back | Client wrote a replicated value locally; server truth overwrote it. |
| Split-screen HUD uses the wrong pawn | Widget uses player index 0 or global getters instead of Owning Player. |
| Updates fire twice | Widget bound more than once without unbinding on rebuild/respawn. |
Debug by printing widget owner, local player/controller, pawn, PlayerState, GameState, net mode, and whether the server or client changed the value.
What HUD widgets do not do¶
- They do not replicate.
- They do not own authoritative gameplay state.
- They do not make GameMode readable on clients.
- They do not automatically rebind after possession or respawn.
- They do not receive old dispatcher calls that happened before binding.
- They do not make private data safe just because it is displayed locally.
- They do not replace server validation for interact, score, inventory, or objective changes.
Lookalikes - which one do I want?¶
| Owner/tool | Use when | Watch out |
|---|---|---|
| HUD / viewport widget | Local player needs screen UI. | Local only; not replicated truth. |
| PlayerController | Local input, UI creation, owner-only client flow. | Other clients do not have every PlayerController. |
| Pawn / Character | Current body state such as health, movement, weapon component. | Can be replaced on death/respawn. |
| PlayerState | Public per-player score/team/name/ready state. | Not private settings or current body-only state. |
| GameState | Public match timer/phase/team score/objective state. | Not per-player private or one-body state. |
| RepNotify | Local copies should refresh presentation when replicated values arrive. | Not a client request path and not a reliable history of every intermediate value. |
| Event Dispatcher | Local systems/widgets need to hear a local state change. | Does not cross the network by itself. |
Rule of thumb: create widgets locally, replicate state deliberately, and bind the HUD to the object that owns each value.
Going deeper¶
- Create Widget + Add to Viewport - local HUD creation and owning player setup.
- Updating a Widget: Bindings vs Events - event-driven UI refresh and initial updates.
- GameState and PlayerState in practice - public match/player state ownership.
- RepNotify variables and local presentation - state replication driving local UI.
- Multiplayer roles - which machines have which framework objects.
- Nameplates, floating health bars, and damage numbers - actor-attached and projected UI reading the same state owners.
- Crosshair, reticle, and interaction prompts - local prompt data and server interaction validation.
- Official docs: User Interfaces and HUDs, Game Mode and Game State, Replicate Actor Properties, AGameStateBase, and APlayerState.
- Engine source (requires engine access - we do not reproduce it here):
UUserWidgetviewport and owning-player behavior lives inEngine/Source/Runtime/UMG/Private/UserWidget.cpp; GameState and PlayerState declarations live inEngine/Source/Runtime/Engine/Classes/GameFramework/GameStateBase.h,GameState.h, andPlayerState.h; replication dispatch is handled by engine networking code such asFObjectReplicatorinEngine/Source/Runtime/Engine/Private/DataReplication.cpp.