Round-Persistent Survival Timers in Verse
What you'll learn
- Defining a
<persistable>class for cross-session data - Binding data to individual players with a module-scoped
weak_map - Safely acquiring each player's UI with
GetPlayerUI[](a<transacts><decides>call) - Building a real
canvas/text_blockwidget and updating it from aTickEventloop
How it works
Verse's persistence system marks a custom class with <persistable>. When you store instances in a module-scoped weak_map(player, your_type), the engine loads a player's data when they join and saves it as it changes — so survival time doesn't vanish on a round restart.
Because a weak_map is keyed by player, reading it is fallible: if (Stats := GlobalTimers[P]): binds the value only when it exists. If there's no entry yet, we build a fresh instance and set GlobalTimers[P] = .... A weak_map does not hold mutable references, so to accumulate time we build a NEW player_stats each update and write it back — the two-step persistent-update pattern.
To show the value on screen we call GetPlayerUI[Player]. Its real signature is GetPlayerUI<native>(Player:player)<transacts><decides>:player_ui, so it is a <decides> call — bind it with [] inside an if. We then AddWidget a canvas containing a text_block, keep a handle to that text_block, and rewrite its text each frame using SetText and interpolation.
Let's build it
This device creates persistent per-player timers, shows each player's survival time in their HUD, and accumulates time every frame — surviving round transitions and reconnects.
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/UI }
using { /Verse.org/Simulation }
using { /Verse.org/Verse }
using { /UnrealEngine.com/Temporary/SpatialMath }
# Persistable type — survives rounds and session reloads.
player_stats := class<final><persistable>:
TimeAlive:float = 0.0
# Module-scoped weak_map binds each player to their saved data.
var GlobalTimers:weak_map(player, player_stats) = map{}
round_persistent_timer := class<concrete>(creative_device):
# Text_block DefaultText expects a message; interpolation makes one.
MakeLabelText<localizes>(Value:int):message = "Surviving: {Value}s"
OnBegin<override>()<suspends>:void =
# Give every current player a persistent stat entry + on-screen label.
for (P : GetPlayspace().GetPlayers()):
# Ensure a persistent entry exists (fallible map read binds only if present).
if (not GlobalTimers[P]):
if (set GlobalTimers[P] = player_stats{}) {}
# GetPlayerUI is <transacts><decides> — bind it with [].
if (UI := GetPlayerUI[P]):
Label := text_block{DefaultText := MakeLabelText(0)}
Root := 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, Right := 0.0, Bottom := 0.0}
Alignment := vector2{X := 0.0, Y := 0.0}
Widget := Label
UI.AddWidget(Root)
# Per-player async loop: accumulate time and refresh the label.
spawn { UpdateTimer(P, Label) }
# Adds DeltaTime each frame, writes it back to the weak_map, and updates the HUD.
UpdateTimer(P:player, Label:text_block)<suspends>:void =
loop:
# Sleep(0.0) yields one frame; treat that as our tick.
Sleep(0.25)
if (Old := GlobalTimers[P]):
# Build a NEW instance (persistent two-step update) and store it.
New := player_stats{TimeAlive := Old.TimeAlive + 0.25}
if (set GlobalTimers[P] = New) {}
if (Whole := Round[New.TimeAlive]):
Label.SetText(MakeLabelText(Whole))```
## Try it yourself
- **Win condition**: inside `UpdateTimer`, check `if (New.TimeAlive >= 60.0):` and `Print("Survivor!")` (or `Trigger` a linked `trigger_device`).
- **Round hooks**: use `GetPlayspace()` entity + `GetFortRoundManager[]` and `SubscribeRoundStarted` to award bonus time when a new round begins.
- **Clear progress**: on a `trigger_device.TriggeredEvent`, rebuild the entry with `set GlobalTimers[P] = player_stats{}` to reset a player's timer.
## Recap
We combined a `<persistable>` class with a module-scoped `weak_map` to store data that outlives round resets. Because map reads and `GetPlayerUI` are both fallible, we bound them with `[]` inside `if`. A `canvas` + `text_block` gave us a real HUD widget, and a per-player `<suspends>` loop accumulated time using the persistent two-step update — build a new instance, then `set GlobalTimers[P] = New`.
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 Implementing Round-Persistent Survival Timers Using PersistentData 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.
References
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.