Persistent Playtime Tracker with Live HUD in Verse
Tutorial intermediate compiles

Persistent Playtime Tracker with Live HUD in Verse

Updated intermediate Code verified

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_map safely with if (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 Sleep loop with spawn{}.
  • Player UI: pushing a text_block onto each player's screen via GetPlayerUI[] + 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.

Comments

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