Verse Wiki — the Verse handbook for Blueprint authors
Bonus · EXTRA

<persistable>: Player Data That Outlives the Session

Leaderboards, coins, quest progress — the reason it is all still there when a player logs out and back in is the persistable badge plus weak_map. This page wires the "dry angle-bracket badge" straight into the feature you most want to build: a save (SaveGame) system — one per player, still there after quit-and-rejoin.

1. The Persistence Kit

An ordinary Verse variable lives exactly one game session: the round ends, the server shuts down, everything zeroes out. To make data survive across sessions, the official recipe is built from three parts:

player_save.verse
using { /Verse.org/Simulation }

# Save class: must be final + persistable, constant fields only
player_save := class<final><persistable>:
    Coins:int = 0
    Wins:int = 0

# Persistent data must live in a module-level weak_map
var PlayerSaves:weak_map(player, player_save) = map{}

Translating the picture: the top half, player_save, is the template for "what one save file looks like" — wearing the <final><persistable> badge pair, with two constants Coins and Wins, both defaulted to 0. The bottom half, PlayerSaves, is the roster of "who saved what" (the weak_map), using players as the index and the save template as the content; it sits at the module's outermost level, which is what lets the engine automatically read and write it as players come and go.

Why only constants, never var? Recall the immutability philosophy from the struct lesson: a save gets written into the player's record, and "immutable" means every save is a complete, self-consistent snapshot — a "half-changed" in-between state can never exist.

2. Reading & Writing: Immutable Updates

If fields cannot change, how do you "add coins"? The old "never patch the old one, just swap in a new one" recipe: build a new save carrying the new numbers, and replace the old one wholesale. Both reads and writes go through "might not go through" checks — the player may not have a save at all, so reading from and writing into the weak_map are both "might not pass" operations, and a Branch has to catch them:

save_station_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

save_station_device := class(creative_device):

    # First time we see a player: hand out a default save
    InitSave(Player:player):void =
        if (not PlayerSaves[Player]):
            if (set PlayerSaves[Player] = player_save{}):
                Print("New player save created")

    # Adding coins = build a new snapshot, replace wholesale
    GiveCoins(Player:player, Amount:int):void =
        if (Old := PlayerSaves[Player]):
            New := player_save{Coins := Old.Coins + Amount, Wins := Old.Wins}
            if (set PlayerSaves[Player] = New):
                Print("Saved, coins now: {New.Coins}")

Translating the picture: InitSave is "first time we meet a player, hand them a blank save" — a Branch first confirms there is nothing on file under their name (not PlayerSaves[Player]), and if so, slots a default player_save{} into the roster. GiveCoins is "add coins" — pull out their old save, build a new one modeled on it with Coins topped up and Wins untouched, then write the whole thing back over the old entry in the roster. At no point does anything get "changed in place" — only "build new, swap out old".

Two details deserve a pause: the read PlayerSaves[Player] must be caught by a Branch (if) — the player may have no save; the write set PlayerSaves[Player] = New (one Set node) may likewise not pass, and must also sit somewhere that can catch it. Every step of saving stands on the "might not go through" machinery you have already learned — this is how Verse's pieces of knowledge interlock.

3. weak_map's Deliberate Handicaps and Schema Evolution

Coming from ordinary map, you will quickly notice weak_map shrank away a pile of abilities: no looping over it (no ForEach), no Length, no stitching (ConcatenateMaps) — all you get is one-to-one reads and writes while clutching a specific player key. That is not corner-cutting; it is deliberate design: save data is stored sharded per player, and the engine only loads a player's share while that player is present; denying you "flip through every player's records one by one" is what keeps the experience from collapsing as the player count balloons (for a server-wide leaderboard, there is a separate dedicated pipeline).

Another real engineering question: the game updates and the save needs one more column of data — now what? Official best practice offers two paths: give the new column a default value — the engine automatically lines up old-version saves with the new layout, so veteran players come back to "old data + defaults in the new column"; going further, declare columns you will only enable later as option types (?t, meaning "might be there, might not"), so the save reserves room for future expansion from day one. Think one version ahead when designing your save layout, and you spare yourself one painful data move after launch.

Which of these save-able-class declarations will pass Compile?

Why does weak_map refuse to let you loop over it (ForEach), and give you no Length?

Compiled from official Epic documentation: Using Persistable Data in Verse ↗, Verse Persistence Best Practices ↗, Struct in Verse ↗.