Round-Persistent Survival Timers in Verse
Tutorial intermediate compiles

Round-Persistent Survival Timers in Verse

Updated intermediate Code verified

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_block widget and updating it from a TickEvent loop

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.

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