Overview
A per-tick HUD is the pattern of showing custom 2D widgets on a player's screen and refreshing what they display on every simulation update (tick). Fortnite's built-in HUD only shows so much — health, ammo, the storm. When you want to surface your game state (doubloons collected, distance to the cove, how long a plank has left before it snaps), you build your own widget tree and attach it to each player's player_ui.
The game problem it solves: you have state that changes continuously — a score climbing, a countdown falling, a position drifting — and you want the player to see it live, not one frame late. Reach for this when a static text device isn't enough and you need a widget that reacts to code you own.
The key pieces from the live digest:
GetPlayerUI(Player:player)— gets theplayer_uifor a player (it fails, so unwrap it).player_ui.AddWidget(Widget:widget)— attaches a widget (like acanvas) to that player's screen.canvas— a positioning container;AddWidget/RemoveWidgetmanage its slots.
We pair those with a score_manager_device so the HUD reflects real awarded points, and with a sleep-driven loop in OnBegin to do the per-tick refresh. Picture a bright cel-shaded cove at high noon: gulls, turquoise water, a plank dock — and a golden "Doubloons: 12" banner glowing at the top of every pirate's screen.
API Reference
player
Full public surface, resolved verbatim from the live Epic digest (Verse.digest.verse). Inherited members are merged from agent.
player<native><public> := class<unique><persistent><module_scoped_var_weak_map_key><epic_internal>(agent):
vector3
3-dimensional vector with
floatcomponents.
Full public surface, resolved verbatim from the live Epic digest (Verse.digest.verse).
vector3<native><public> := struct<concrete><computes><persistable><uht_comparable>:
Walkthrough
Here's the full scene. When a player steps on a trigger_device at the end of the dock, we award a point through a score_manager_device. Meanwhile a per-player async loop rebuilds each player's HUD banner every tick so the doubloon count is always current.
using { /Fortnite.com/Devices }
using { /Fortnite.com/Playspaces }
using { /UnrealEngine.com/Temporary/UI }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /Verse.org/Simulation }
using { /Fortnite.com/UI }
# A live cel-shaded pirate cove HUD: a doubloon banner refreshed every tick.
cove_hud_device := class(creative_device):
# The plate at the end of the dock that hands out doubloons.
@editable
DoubloonPlate : trigger_device = trigger_device{}
# The score device that actually tracks each pirate's doubloons.
@editable
Doubloons : score_manager_device = score_manager_device{}
# Localized text helper — message params need a localized value, not a raw string.
BannerText<localizes>(Count:int):message = "Doubloons: {Count}"
# Remember each player's canvas so we can swap it out cleanly each tick.
var ActiveCanvases : [player]canvas = map{}
OnBegin<override>()<suspends>:void =
# When a pirate steps on the dock plate, award a doubloon.
DoubloonPlate.TriggeredEvent.Subscribe(OnPlateStepped)
# Start a live HUD loop for everyone already in the playspace.
for (Player : GetPlayspace().GetPlayers()):
spawn { RunHudLoop(Player) }
# Award a point through the score manager when the plate fires.
OnPlateStepped(Agent : ?agent):void =
if (A := Agent?):
Doubloons.Activate(A)
# Per-player loop: rebuild the banner every tick.
RunHudLoop(Agent : agent)<suspends>:void =
loop:
RefreshBanner(Agent)
# Sleep(0.0) yields until the next simulation tick.
Sleep(0.0)
# Build a fresh banner widget and push it to this player's screen.
RefreshBanner(Agent : agent):void =
if:
Player := player[Agent]
UI := GetPlayerUI[Player]
then:
# Remove last tick's canvas so we don't stack widgets forever.
if (Old := ActiveCanvases[Player]):
UI.RemoveWidget(Old)
# Read the live doubloon count for this pirate.
Count := GetDoubloonCount(Agent)
NewCanvas := canvas:
Slots := array:
canvas_slot:
Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.05}, Maximum := vector2{X := 0.5, Y := 0.05}}
Offsets := margin{Top := 0.0, Left := 0.0, Right := 0.0, Bottom := 0.0}
Alignment := vector2{X := 0.5, Y := 0.0}
Widget := text_block{DefaultText := BannerText(Count)}
UI.AddWidget(NewCanvas)
set ActiveCanvases[Player] = NewCanvas
# Placeholder read of state — in a real game read from your own tally.
GetDoubloonCount(Agent : agent):int =
0
Line by line:
@editable DoubloonPlate/Doubloons— these are the two placed devices. You MUST declare placed devices as@editablefields; calling a baretrigger_device.TriggeredEventwould be an unknown identifier.BannerText<localizes>(Count:int):message— amessageparam (whichtext_block.DefaultTextwants) takes a localized value. This helper interpolates the count and returns amessage. There is noStringToMessage.var ActiveCanvases : [player]canvas— a map of each player's current canvas so we canRemoveWidgetthe old one before adding the new one.OnBeginsubscribes the plate'sTriggeredEventandspawns a per-player HUD loop for everyone in the playspace.OnPlateSteppedunwraps the?agentwithif (A := Agent?)then callsDoubloons.Activate(A)— the score manager's real method to grant that pirate a point.RunHudLoopis a<suspends>method: aloopthat refreshes thenSleep(0.0)s, which yields until the next tick — this is the "each tick" heartbeat.RefreshBannerusesGetPlayerUI[Player](note the[]— it decides/fails), removes the old canvas, builds a newcanvaswith a centeredtext_block, thenUI.AddWidgetpushes it and we store it in the map.
Common patterns
Pattern 1 — Add a HUD once, without rebuilding it every tick. If your widget rarely changes, attach it a single time in OnBegin and skip the loop.
using { /Fortnite.com/Devices }
using { /Fortnite.com/Playspaces }
using { /UnrealEngine.com/Temporary/UI }
using { /Verse.org/Simulation }
welcome_hud_device := class(creative_device):
WelcomeText<localizes>(S:string):message = "{S}"
OnBegin<override>()<suspends>:void =
for (Player : GetPlayspace().GetPlayers()):
if (P := player[Player], UI := GetPlayerUI[P]):
Banner := canvas:
Slots := array:
canvas_slot:
Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.1}, Maximum := vector2{X := 0.5, Y := 0.1}}
Alignment := vector2{X := 0.5, Y := 0.0}
Widget := text_block{DefaultText := WelcomeText("Welcome to Sunny Cove!")}
UI.AddWidget(Banner)
Pattern 2 — Drive the HUD from a score event instead of a raw tick. Subscribe to ScoreOutputEvent so the banner only rebuilds when points are actually awarded — cheaper than every frame.
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/UI }
using { /Verse.org/Simulation }
score_hud_device := class(creative_device):
@editable
Doubloons : score_manager_device = score_manager_device{}
CountText<localizes>(N:int):message = "Score updated! +{N}"
OnBegin<override>()<suspends>:void =
Doubloons.ScoreOutputEvent.Subscribe(OnScored)
OnScored(Agent : agent):void =
if (P := player[Agent], UI := GetPlayerUI[P]):
Flash := canvas:
Slots := array:
canvas_slot:
Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.15}, Maximum := vector2{X := 0.5, Y := 0.15}}
Alignment := vector2{X := 0.5, Y := 0.0}
Widget := text_block{DefaultText := CountText(1)}
UI.AddWidget(Flash)
Pattern 3 — Clean up a player's HUD. Track the canvas and RemoveWidget it (e.g. when a player leaves a zone or the round ends).
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/UI }
using { /Verse.org/Simulation }
hud_cleanup_device := class(creative_device):
@editable
ExitPlate : trigger_device = trigger_device{}
var Shown : [player]canvas = map{}
HintText<localizes>(S:string):message = "{S}"
OnBegin<override>()<suspends>:void =
ExitPlate.TriggeredEvent.Subscribe(OnLeave)
OnLeave(Agent : ?agent):void =
if (A := Agent?, P := player[A], UI := GetPlayerUI[P]):
if (Old := Shown[P]):
UI.RemoveWidget(Old)
Gotchas
GetPlayerUIfails — unwrap it. It's<decides>, so call it with brackets inside anif/failure context:if (UI := GetPlayerUI[Player]). Using()on a<decides>function is a compile error.agentis notplayer.GetPlayerUIneeds aplayer, but device events hand you anagent. Convert withplayer[Agent](also failable) before callingGetPlayerUI.message, notstring.text_block.DefaultTextand other UI text want a localizedmessage. Use a<localizes>helper likeBannerText<localizes>(Count:int):message = "...". There is noStringToMessage.- Rebuilding every tick stacks widgets.
AddWidgetadds — it never replaces. If you refresh each tick you MUSTRemoveWidgetthe previous canvas first, or you'll pile hundreds of banners on screen. Track the current one in a[player]canvasmap. Sleep(0.0)yields one tick. That's how you get a per-tick heartbeat inside a<suspends>loop. Don'tsleep(0.0)in a tight loop that also does heavy work — it runs every frame and can cost performance; prefer event-driven updates when the state changes rarely.- Per-player loops need a player. Only real players have a
player_ui; there's no HUD for a generic agent like an AI guard. Guard your loop withplayer[Agent]first.