Verse Player Data: Persistent Maps & Per-Player HUD
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
- Persistable data: We declare a
class<final><persistable>calledplayer_statsand a module-scopedvar PlayerDataMap : weak_map(player, player_stats) = map{}. Module-scopedweak_maps keyed byplayerwith a persistable value are how Verse synchronizes save data — no manual serialization required. - 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 withset PlayerDataMap[Player] = NewStats— persistable structs are not mutated field-by-field. - Per-player UI: For each player we call
GetPlayerUI[Player](fallible, so bound in anif), build atext_block, wrap it in acanvas, and callPlayerUI.AddWidget(Canvas). Each player only ever sees their own widget. - 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_deviceinto your map and assign it to theScoreButton@editableslot, then press it in a playtest to watch each player's HUD climb. - Add a second field (e.g.
Visits : int = 0) toplayer_statsand increment it inOnBeginper joining player — remember you can only ADD fields, never remove them after publishing. - Move the HUD by changing the
Anchorsvalues (0.0–1.0 are fractions of the screen) to reposition the score in a corner. - Give the text color by setting
DefaultTextColoron thetext_blockusing 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.