Verse Player Data: Persistent Maps & Per-Player HUD
Tutorial intermediate compiles

Verse Player Data: Persistent Maps & Per-Player HUD

Updated intermediate Code verified

What you'll learn

We will build a device that maintains a persistent Score for every player in the playspace. A module-level weak_map(player, player_stats) stores the data per player, per island — it loads automatically when a player joins and saves when their data changes. We will then use the Player UI API (GetPlayerUI[] + AddWidget) to render each player's own score onto their personal screen, and bump the score whenever they trigger a button.

How it works

  1. Persistable data: We declare a class<final><persistable> called player_stats and a module-scoped var PlayerDataMap : weak_map(player, player_stats) = map{}. Module-scoped weak_maps keyed by player with a persistable value are how Verse synchronizes save data — no manual serialization required.
  2. Agent-keyed access: Reading and writing the map is fallible, so we bind it inside an if/else. To update a persistable struct we rebuild it (player_stats{ Score := N }) and write it back with set PlayerDataMap[Player] = NewStats — persistable structs are not mutated field-by-field.
  3. Per-player UI: For each player we call GetPlayerUI[Player] (fallible, so bound in an if), build a text_block, wrap it in a canvas, and call PlayerUI.AddWidget(Canvas). Each player only ever sees their own widget.
  4. Driving updates: We subscribe to a button's InteractedWithEvent. On each interaction we increment that player's score, persist it, and refresh their HUD widget.

Let's build it

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

# Persistable struct holding one player's stats.
# Once published, fields may be ADDED (with defaults) but never removed/retyped.
player_stats := class<final><persistable>:
    Score : int = 0

# Module-scoped weak_map keyed by player -> persistable value.
# Loads automatically on join, saves automatically on change.
var PlayerDataMap : weak_map(player, player_stats) = map{}

score_hud_device := class<concrete>(creative_device):

    # Players press this button to earn a point.
    @editable
    ScoreButton : button_device = button_device{}

    # Track each player's live HUD text so we can refresh it.
    var Labels : [player]text_block = map{}

    OnBegin<override>()<suspends>: void =
        # Subscribe to the button; handler runs each interaction.
        ScoreButton.InteractedWithEvent.Subscribe(OnButtonPressed)

    # Handler signature must match (Agent:agent):void from the button event.
    OnButtonPressed(Agent : agent): void =
        # Recover the player from the agent (fallible -> bind in if).
        if (Player := player[Agent]):
            # Load existing stats, or start fresh.
            CurrentScore := if (Stats := PlayerDataMap[Player]) then Stats.Score else 0
            NewScore := CurrentScore + 1
            # Persist: rebuild the struct, then write it back.
            if (set PlayerDataMap[Player] = player_stats{ Score := NewScore }):
                ShowScore(Player, NewScore)

    # Build (or refresh) this player's personal HUD widget.
    ShowScore(Player : player, Score : int): void =
        # Reuse the existing label if we already made one.
        if (Label := Labels[Player]):
            Label.SetText(StringToMessage("Score: {Score}"))
        else if (PlayerUI := GetPlayerUI[Player]):
            # First time: create a text block inside a canvas slot.
            NewLabel := text_block{ DefaultText := StringToMessage("Score: {Score}") }
            Canvas := canvas:
                Slots := array:
                    canvas_slot:
                        Anchors := anchors{ Minimum := vector2{X := 0.05, Y := 0.05}, Maximum := vector2{X := 0.05, Y := 0.05} }
                        Offsets := margin{ Top := 0.0, Left := 0.0 }
                        Widget := NewLabel
            PlayerUI.AddWidget(Canvas)
            if (set Labels[Player] = NewLabel) {}

    # Helper: turn a string into a message for widget text.
    StringToMessage<localizes>(Value : string): message = "{Value}"

Try it yourself

  • Drop a button_device into your map and assign it to the ScoreButton @editable slot, then press it in a playtest to watch each player's HUD climb.
  • Add a second field (e.g. Visits : int = 0) to player_stats and increment it in OnBegin per joining player — remember you can only ADD fields, never remove them after publishing.
  • Move the HUD by changing the Anchors values (0.0–1.0 are fractions of the screen) to reposition the score in a corner.
  • Give the text color by setting DefaultTextColor on the text_block using a color from /Verse.org/Colors.

Recap

You built a fully working per-player persistence system: a module-scoped weak_map(player, player_stats) that loads and saves automatically, agent-to-player recovery via player[Agent], the rebuild-and-write pattern for updating persistable structs, and the Player UI API (GetPlayerUI[] + AddWidget + text_block/canvas) to render each player's own data. The keys: map access is fallible (bind it in if), and persistable structs are replaced wholesale rather than mutated field-by-field.

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 Storing Per-Player Data Using Agent-Keyed Maps in Verse 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