Reference Verse

Update HUD Each Tick: Live On-Screen Widgets in Verse

Ever wanted a treasure-count banner that ticks up the instant a player grabs another doubloon, or a distance-to-the-cove readout that updates as they row? That's a per-tick HUD. In this article we build a live cel-shaded HUD on a sunny pirate cove, pushing a fresh widget to each player's screen every game tick using GetPlayerUI, canvas, and the player_ui AddWidget API.

Updated
The code on this reference page is provided as-is and did not pass the latest compile check — treat the examples as a starting point and verify in your project.

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 the player_ui for a player (it fails, so unwrap it).
  • player_ui.AddWidget(Widget:widget) — attaches a widget (like a canvas) to that player's screen.
  • canvas — a positioning container; AddWidget/RemoveWidget manage 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 float components.

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 @editable fields; calling a bare trigger_device.TriggeredEvent would be an unknown identifier.
  • BannerText<localizes>(Count:int):message — a message param (which text_block.DefaultText wants) takes a localized value. This helper interpolates the count and returns a message. There is no StringToMessage.
  • var ActiveCanvases : [player]canvas — a map of each player's current canvas so we can RemoveWidget the old one before adding the new one.
  • OnBegin subscribes the plate's TriggeredEvent and spawns a per-player HUD loop for everyone in the playspace.
  • OnPlateStepped unwraps the ?agent with if (A := Agent?) then calls Doubloons.Activate(A) — the score manager's real method to grant that pirate a point.
  • RunHudLoop is a <suspends> method: a loop that refreshes then Sleep(0.0)s, which yields until the next tick — this is the "each tick" heartbeat.
  • RefreshBanner uses GetPlayerUI[Player] (note the [] — it decides/fails), removes the old canvas, builds a new canvas with a centered text_block, then UI.AddWidget pushes 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

  • GetPlayerUI fails — unwrap it. It's <decides>, so call it with brackets inside an if/failure context: if (UI := GetPlayerUI[Player]). Using () on a <decides> function is a compile error.
  • agent is not player. GetPlayerUI needs a player, but device events hand you an agent. Convert with player[Agent] (also failable) before calling GetPlayerUI.
  • message, not string. text_block.DefaultText and other UI text want a localized message. Use a <localizes> helper like BannerText<localizes>(Count:int):message = "...". There is no StringToMessage.
  • Rebuilding every tick stacks widgets. AddWidget adds — it never replaces. If you refresh each tick you MUST RemoveWidget the previous canvas first, or you'll pile hundreds of banners on screen. Track the current one in a [player]canvas map.
  • Sleep(0.0) yields one tick. That's how you get a per-tick heartbeat inside a <suspends> loop. Don't sleep(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 with player[Agent] first.

Guides & scripts that use player

Step-by-step tutorials that put this object to work.

Build your own lesson with player

Generate a personalized, step-by-step lesson plan built around this object — grounded in this exact reference and our compile-verified knowledge base.

Build a lesson →