Persistent Playtime Tracker with Live HUD in Verse
What you'll learn
- Module-scoped persistence: linking players to saved stats with
weak_map(player, T)and a<persistable>class. - Fallible map access: reading/updating a
weak_mapsafely withif (S := Map[P]). - The two-step persist update: build a fresh instance, then
set Map[Key] = ...— you cannot mutate a persistable field in place. - Concurrent timers: launching an independent
Sleeploop withspawn{}. - Player UI: pushing a
text_blockonto each player's screen viaGetPlayerUI[]+AddWidget.
How it works
Persistable data lives in a module-scoped weak_map
Verse saves per-player data by storing a <persistable> class inside a module-scoped weak_map(player, T). When a player joins, their entry loads automatically; when it changes, it saves. Because persistable class fields are immutable through the map, you never mutate a field in place — you build a new instance and reassign the whole entry.
player_progress := class<final><persistable>:
CumulativePlayTime : int = 0
Fallible map access
Map indexing is fallible — a player may have no entry yet. Always bind it:
Concurrent timers
Sleep requires a <suspends> context. To run a background clock without blocking OnBegin, launch it with spawn{ PlaytimeLoop() }. The loop ticks every second, updates persistence, and refreshes each player's HUD.
Player UI
Player.GetPlayerUI[] is fallible — bind it in an if. Then AddWidget a text_block you keep a reference to so you can SetText on it each tick.
Let's build it
The device below tracks cumulative playtime, saves it persistently, shows a live HUD counter per player, and lets a trigger grant a persistent reward.
using { /Fortnite.com/Devices }
using { /Fortnite.com/UI }
using { /UnrealEngine.com/Temporary/UI }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /Verse.org/Simulation }
using { /Verse.org/Colors }
# Persistable per-player save data (module scope required for the weak_map)
player_progress := class<final><persistable>:
CumulativePlayTime : int = 0
RewardsClaimed : int = 0
# Module-scoped weak_map: auto-saves/loads per player across sessions
var ProgressMap : weak_map(player, player_progress) = map{}
persistent_hud_device := class<concrete>(creative_device):
@editable Trigger : trigger_device = trigger_device{}
# Keeps each player's live HUD text widget so we can update it every tick
var HudTexts : [player]text_block = map{}
OnBegin<override>()<suspends>: void =
# Build a HUD counter for everyone already present
for (Player : GetPlayspace().GetPlayers()):
AttachHud(Player)
# Reward on trigger; store the subscription so it can be cancelled later
Sub := Trigger.TriggeredEvent.Subscribe(OnTriggered)
# Run the persistence clock concurrently so OnBegin isn't blocked
spawn{ PlaytimeLoop() }
# Creates a text_block and pushes it onto the player's screen via GetPlayerUI
AttachHud(Player : player) : void =
if (UI := GetPlayerUI[Player]):
NewText := text_block{DefaultTextColor := NamedColors.White}
UI.AddWidget(NewText)
if (set HudTexts[Player] = NewText) {}
# Background clock: ticks every second, persists playtime, refreshes HUD
PlaytimeLoop()<suspends>: void =
loop:
Sleep(1.0)
for (Player : GetPlayspace().GetPlayers()):
# Read current value (default 0 if no entry yet)
var Seconds : int = 0
var Rewards : int = 0
if (S := ProgressMap[Player]):
set Seconds = S.CumulativePlayTime
set Rewards = S.RewardsClaimed
# Two-step persist update: build new instance, reassign whole entry
NewProgress := player_progress:
CumulativePlayTime := Seconds + 1
RewardsClaimed := Rewards
if (set ProgressMap[Player] = NewProgress) {}
# Update this player's on-screen counter
if (Txt := HudTexts[Player]):
Txt.SetText(StringToMessage("Playtime: {Seconds + 1}s Rewards: {Rewards}"))
# Grants a persistent reward when a player uses the trigger
OnTriggered(Agent : ?agent) : void =
if (A := Agent?, Player := player[A]):
var Seconds : int = 0
var Rewards : int = 0
if (S := ProgressMap[Player]):
set Seconds = S.CumulativePlayTime
set Rewards = S.RewardsClaimed
NewProgress := player_progress:
CumulativePlayTime := Seconds
RewardsClaimed := Rewards + 1
if (set ProgressMap[Player] = NewProgress) {}
# Helper: text_block.SetText needs a message, not a raw string
StringToMessage<localizes>(Value : string) : message = "{Value}"```
## Try it yourself
- Drop the device in your island and link a **Trigger Device** to its `Trigger` field.
- Play, wait a few seconds, and watch the HUD counter climb; use the trigger to bump `Rewards`.
- Leave the session and rejoin — the saved playtime reloads from persistence.
- Extend `player_progress` by ADDING a new field with a default (e.g. `HighScore:int = 0`) — never remove or retype existing fields, or persistence breaks.
- Cancel `Sub` (with `Sub.Cancel()`) in a cleanup path when you want to stop rewarding.
## Recap
You linked players to a `<persistable>` class through a module-scoped `weak_map`, updated it with the mandatory build-new-instance-then-reassign pattern, ran a concurrent `Sleep` loop with `spawn{}`, and drove a real per-player HUD with `GetPlayerUI[]` + `AddWidget` + `SetText`. That combination is the backbone of loyalty systems, tycoons, and roguelites that remember players between sessions.
Get the complete code — free
You've read the full walkthrough. The complete, copy-paste-ready Verse solution is free for members — sign in to unlock it.
Free with your BrainDead.TV / BrainDeadGuild Discord account. The walkthrough above is always free.
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 Persistent Session Data & Concurrent Timer Management 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.