Tutorial 02 · Persistence

Player progress that survives leaving

A player earns something, leaves, comes back tomorrow, and it's still there. This is the single most asked-for thing after "how do I make a concert", and it has two failure modes that will find you if you don't design for them from day one.

Draft · verify code before publishing
Player HUD showing currency restored after rejoining the island
7steps, in build order
1weak_map is usually enough
2real errors, with the fix
∞how long a saved field lives

Before step one

  • New to UEFN? Do tutorial 00: setup first. It covers the software, your Epic developer account and how to add a Verse file.
  • An island with at least one thing worth saving (currency, a level, an unlock).
  • Comfort pasting a Verse file into your project and building it.
  • A second account or tester, so you can test leaving and rejoining while the session keeps running.
  • Know whether your island has been published before. From that moment, the save format is permanent.

Download the Verse files

Complete files used in this tutorial; each works on its own. How to add one: setup step 7. Build-check in UEFN pending.

Download all (.zip)
  • .vtycoon_currency_manager.versePersistent save class, currency, passive income, autosave with connection guardDownload
01

Decide what to save, permanently

a short list of fields you're willing to keep forever

A <persistable> class is append-only from the moment the island is first published. Adding a field later is safe. Removing or renaming one fails publish validation, because live save data still references it.

So treat every persisted field as permanent on the day you add it. Write the list down before you write code. Keep it short; you can always add.

Diagram: player → save class → fields (Currency, Level)
Trap

"I'll clean up the names later" isn't an option. A field called XP that you stop using stays in the file forever.

Done when

You have a written list of field names and types, and you'd be fine seeing every one of them in the file a year from now.

02

Write the save class

one class that holds everything a player keeps

The class needs <final><persistable>. Give every field a default, so a brand-new player gets a sensible starting state.

pattern — save class
# Append-only after first publish: never remove or rename a field.
player_save := class<final><persistable>:
    Currency : int = 0
    Level    : int = 0
Done when

A Verse build of the project finishes with no diagnostics.

03

Spend exactly one weak_map on it

all per-player save data lives in a single map

Verse allows only a small number of persistent weak_map variables per island (four, in the project this was learned in). Spend them deliberately. One map holding one save class is usually enough, and it leaves room for later.

pattern — the map (module level)
# Verse allows only a small number of weak_map variables per island
# (4 in the project this was learned in). Spend them deliberately --
# one map holding one save class is usually enough.
var PlayerSave : weak_map(player, player_save) = map{}
Note

The exact limit is unconfirmed beyond the project it was seen in. Check current Epic documentation before you plan around a number.

Done when

Your project has one persistent weak_map for player saves, not one per stat.

04

Restore progress when a player joins

a returning player sees their saved values on their HUD within seconds of joining

When a player joins, look them up in PlayerSave. If there's an entry, load it into the runtime maps your game uses. If there isn't, they're new and get the defaults.

tycoon_currency_manager.verse — restore on join
InitPlayer(Player : player) : void =
    if (Saved := PlayerSave[Player]):
        # Returning player: load what they had.
        if (set PlayerCurrency[Player] = Saved.Currency) {}
        if (set PlayerLevel[Player] = Saved.Level) {}
    else:
        # New player: defaults.
        if (set PlayerCurrency[Player] = StartingCurrency) {}
        if (set PlayerLevel[Player] = 0) {}

The download calls this for everyone already in the session and for every player who joins later (PlayerAddedEvent).

tycoon_currency_manager.verseThe complete file, ready to paste into your project. Download full file
HUD currency panel before leaving and after rejoining: the saved values come back (plus a few seconds of passive income)
Done when

You earn some currency, leave the island, rejoin, and the HUD shows the same amount.

05

Write changes with the set idiom

every change is written into the map without compile errors

Assigning into a map can fail, so it has to sit inside an if. The empty block isn't a mistake: it's how Verse says "I don't care about the failure case".

set idiom
# Map writes can fail, so they live inside an if. Empty block on purpose.
if (set Balance[Player] = 100) {}
Trap

A bare logic isn't a condition. if (Hidden): fails with "Expected an expression that can fail in the 'if' condition clause". Compare explicitly: if (Hidden = true):.

Done when

Earning currency updates the HUD, and the value survives the leave-and-rejoin test from step 4.

06

Autosave, with a connection guard

saves happen on a timer, and stop cleanly the moment a player leaves

This is the bug everybody writes. An autosave loop spawned per player does not stop when that player leaves. It keeps ticking against a now-invalid key and throws a runtime error. Guard every iteration.

pattern — autosave
SaveLoop(Player : player)<suspends> : void =
    loop:
        Sleep(SaveIntervalSeconds)
        # Without this check the loop outlives the player and throws.
        if (IsPlayerConnected(Player) = false):
            break
        SavePlayer(Player)

IsPlayerConnected(Player : player)<transacts> : logic =
    for (P : GetPlayspace().GetPlayers()):
        if (P = Player):
            return true
    return false
what it looks like without the guard
ErrRuntime_WeakMapInvalidKey: Invalid key used to access persistent
`var` `weak_map`.
Done when

A tester leaves mid-session and, over several save intervals, the log shows no ErrRuntime_WeakMapInvalidKey.

07

Survive a republish

you can ship an update without breaking everybody's save

After first publish, add fields freely. Never remove or rename one. If you already did, the publish fails with an error naming the missing field:

publish validation error
Missing definition in source package that corresponds to published
definition (/…/player_progress:)XP
verse_validate_publish_error

The fix is to put the field back, even unused, with a comment saying it can never leave.

pattern — save class, after a mistake
player_save := class<final><persistable>:
    Currency : int = 0
    Level    : int = 0
    # Unused. Live save data still references it -- it can never leave.
    XP       : int = 0
Done when

You add a new field, publish an update, and a player who saved before the update rejoins with their old values intact.

Before you publish

Persistence checklist

Every line here is something that really went wrong.