Build Live Player Stat UI in Verse
Tutorial intermediate compiles

Build Live Player Stat UI in Verse

Updated intermediate Code verified

What you'll learn

  • Why GetPlayerUI[Player] is a <decides> call and must live inside a failure context.
  • Building a widget hierarchy with stack_box + stack_box_slot and placing it on a canvas.
  • Displaying it with player_ui.AddWidget(...) and keeping widget references for later.
  • Pushing live text updates from a <suspends> loop using text_block.SetText.

How it works

Player UI is per-player and fallible. GetPlayerUI(Player) is declared <transacts><decides>, so it can fail when a player has no UI. You call it with square brackets inside a failure context — if (PlayerUI := GetPlayerUI[Player]): — and the bound PlayerUI is a plain player_ui handle, not an option. Calling it as a bare statement produces compiler error 3512.

Once you hold a player_ui, you build a layout. text_blocks are placed into a vertical stack_box via stack_box_slot, the stack goes on a canvas via a canvas_slot, and PlayerUI.AddWidget(Canvas, player_ui_slot{...}) renders it. To update the display later, you keep references to the created text_blocks in a map<player, ...> and call SetText on them.

Live updates run inside OnBegin, which is <suspends>. That lets us Sleep for pacing and iterate the map each tick, calling SetText with an interpolated message so each player's panel stays in sync with the game state.

Let's build it

Paste this device into UEFN, place it on your island, and play. Each player gets a stat panel whose score climbs every second.

using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/Playspaces }
using { /Verse.org/Simulation }
using { /Fortnite.com/UI }
using { /UnrealEngine.com/Temporary/SpatialMath }

# Holds the widget references we need to update for one player.
player_stat_ui := class:
    ScoreText : text_block
    StatusText : text_block

live_stat_ui_device := class<concrete>(creative_device):

    @editable
    StatTitle : string = "Player Performance"

    # Player -> that player's live text widgets.
    var ActiveUI : [player]player_stat_ui = map{}

    OnBegin<override>()<suspends>: void =
        # Build a panel for every player currently in the playspace.
        for (PlayerItem : GetPlayspace().GetPlayers()):
            # GetPlayerUI is <decides> -> must be used in a failure context.
            if (PlayerUI := GetPlayerUI[PlayerItem]):
                # Vertical stack holds the title + two stat rows.
                Stack := stack_box{ Orientation := orientation.Vertical }

                Title := text_block{ DefaultText := StringToMessage("{StatTitle}") }
                Stack.AddWidget(stack_box_slot{ Widget := Title })

                ScoreLabel := text_block{ DefaultText := StringToMessage("Score: 0") }
                Stack.AddWidget(stack_box_slot{ Widget := ScoreLabel })

                StatusLabel := text_block{ DefaultText := StringToMessage("Status: Ready") }
                Stack.AddWidget(stack_box_slot{ Widget := StatusLabel })

                # Anchor the stack in a canvas so it renders at a fixed spot.
                Screen := canvas{}
                Screen.AddWidget(canvas_slot{
                    Anchors := anchors{ Minimum := vector2{ X := 0.05, Y := 0.2 }, Maximum := vector2{ X := 0.05, Y := 0.2 } },
                    Offsets := margin{ Left := 0.0, Top := 0.0, Right := 200.0, Bottom := 120.0 },
                    Widget := Stack
                })

                # Actually show it on this player's screen.
                PlayerUI.AddWidget(Screen, player_ui_slot{ InputMode := ui_input_mode.None })

                # Remember the widgets so we can update them live.
                if (set ActiveUI[PlayerItem] = player_stat_ui{ ScoreText := ScoreLabel, StatusText := StatusLabel }) {}

        # Live update loop — OnBegin is <suspends>, so Sleep is legal here.
        var Score : int = 0
        loop:
            Sleep(1.0)
            set Score += 15
            for (Key -> UI : ActiveUI):
                UI.ScoreText.SetText(StringToMessage("Score: {Score}"))
                UI.StatusText.SetText(StringToMessage("Status: Active"))

StringToMessage<localizes>(Value : string) : message = "{Value}"```

## Try it yourself

- **Add a third stat:** give `player_stat_ui` an `HPText : text_block` field, add another `stack_box_slot`, and update it in the loop.
- **Toggle visibility:** call `Stack.SetVisibility(widget_visibility.Hidden)` then `widget_visibility.Visible` on a timer to blink the panel.
- **Per-player scores:** store an `int` per player in a second `[player]int` map instead of one global `Score`.
- **Clean up on exit:** in `OnEnd`, look up the player's `player_ui` again and call `RemoveWidget` on the canvas.

## Recap

You instantiated a widget hierarchy at runtime (`text_block` -> `stack_box` -> `canvas`), added it to each player's screen through the fallible `GetPlayerUI[]` handle, and drove live text updates from a `<suspends>` loop. The keys to getting this to compile: use `[]` for the `<decides>` UI lookup inside an `if`, treat the bound `player_ui` as a plain handle, and keep widget references in a map so `SetText` can update the right screen.

Check your understanding

Test yourself with an interactive quiz and track your progress + earn XP — free for members.

Turn this into a guided course

Add Dynamic Player Stat UI via Scene Graph Widgets to your free study plan — we'll suggest related pages and stitch the lot into one compile-checked, self-guided lesson with worked examples and quizzes.

Original tutorial generated by Verse Island from the Verse/UEFN knowledge base, with references to the Epic Games sources above. Code is validated against the knowledge base.

Comments

    Sign in to vote, comment, or suggest an edit. Sign in