The Hall of Badges: Cross-Zone Persistence with [player] Maps
Tutorialintermediatecompiles

The Hall of Badges: Cross-Zone Persistence with [player] Maps

Updated intermediateDevicesCode verified

Welcome to the Map Room, apprentice cartographer. Professor AweShucks has a wall here he is unreasonably proud of: the Hall of Badges, where every zone badge a player has ever earned glows on a brass plaque — even if they earned it three sessions ago and two updates back. The machinery behind that wall is UEFN player persistence: a weak_map(player, ...) holding one <persistable> record per player, saved by the engine itself. This is the same pattern Cinder the dragon taught over in east-volcano's Persisting a Tycoon's State with <persistable> — here we import it wholesale and grow it into the hub's shared IslandProgress profile.

What you will build

The IslandProgress badge profile — the single most load-bearing piece of the AweShucks Town capstone:

  • A <final><persistable> class island_progress with one logic badge flag per zone, a TownCoins balance, and — crucially — a Version:int field.
  • A module-scope var IslandProgressMap : weak_map(player, island_progress) that the engine saves per player, per island, across sessions.
  • A migration function that upgrades old-version saves the moment a returning player joins.
  • A hall_of_badges_device that stamps badges, lights the Hall glow, and reports progress on Professor AweShucks' announcement board.

Downstream, this exact profile is what lesson 13's portal_gatekeeper module checks before opening a paid-zone booth, what the portal beacon component (lesson 15) reads to decide whether to glow, and what the full AweShucks Town capstone (lesson 18) shares island-wide.

Imported from east-volcano: the <final><persistable> record + copy-<constructor> + weak_map idiom is lifted directly from Persisting a Tycoon's State with <persistable> (compile-passed grounding, itself distilled from Epic's Santa's Toy Factory sample). If that lesson's save_data class looks familiar below — good. It should. We are reusing a proven part, not reinventing it.

Walkthrough

Step 1 — Declare the versioned schema

A persistable class must be <final>, and every field must itself be persistable (primitives, other persistable classes/structs, or arrays/maps of those). We give each zone a logic flag and — the star of this lesson — a Version field, so future-you can evolve the schema without orphaning old saves.

Step 2 — Write the copy-constructor

You cannot mutate a field of a stored persistable record in place. Every update rebuilds a fresh record: override the one field you are changing, then let the copy-<constructor> fill in everything else. This "override one, copy the rest" idiom is exactly the east-volcano tycoon pattern.

Step 3 — Store it in a module-scope weak_map

var IslandProgressMap : weak_map(player, island_progress) = map{} is the standard persistence container: the engine saves each player's entry and restores it next session. Reads and writes are failable ([]), because the key may be absent — so every accessor lives in a <decides><transacts> function and guards with if (not IslandProgressMap[Player]).

Step 4 — Migrate on join

EnsureProfile runs when a player joins (and once at OnBegin for anyone already present). If there is no record, it creates one at the current version. If there is an old-version record, MigrateProgress rebuilds it: stamp the new Version, translate anything that changed meaning (here, v2 grants 50 retroactive TownCoins per badge), and copy the rest through the constructor. Old badges survive untouched.

Step 5 — Grant and read badges

GrantBadge stamps a zone flag via StampBadge; the Hall device reads the profile and lights the glow. Both go through the same failable accessors — no raw map pokes anywhere.

Here is the complete, self-contained device. Drop it in a fresh Verse file, wire the four @editable devices in the editor, and play:

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

# ─── The versioned IslandProgress schema ──────────────────────────
# Bump this when the schema changes; MigrateProgress upgrades old saves.
CurrentSaveVersion:int = 2

island_progress := class<final><persistable>:
    Version:int = 0
    ShellBadge:logic = false      # south-shores capstone earned
    JungleBadge:logic = false     # north-jungle capstone earned
    VolcanoBadge:logic = false    # east-volcano capstone earned
    CoveBadge:logic = false       # west-coves capstone earned
    DeepsBadge:logic = false      # the-deeps capstone earned
    TownCoins:int = 0

# Copy-constructor: rebuilds a fresh record from an old one.
MakeIslandProgress<constructor>(Src:island_progress)<transacts> := island_progress:
    Version := Src.Version
    ShellBadge := Src.ShellBadge
    JungleBadge := Src.JungleBadge
    VolcanoBadge := Src.VolcanoBadge
    CoveBadge := Src.CoveBadge
    DeepsBadge := Src.DeepsBadge
    TownCoins := Src.TownCoins

# The persisted store — the engine saves one record per player.
var IslandProgressMap:weak_map(player, island_progress) = map{}

# ─── Pure helpers ─────────────────────────────────────────────────
CountBadges(Profile:island_progress)<transacts>:int =
    var Count:int = 0
    if (Profile.ShellBadge?):
        set Count += 1
    if (Profile.JungleBadge?):
        set Count += 1
    if (Profile.VolcanoBadge?):
        set Count += 1
    if (Profile.CoveBadge?):
        set Count += 1
    if (Profile.DeepsBadge?):
        set Count += 1
    Count

# Old save -> current schema. v0/v1 badge holders get retroactive coins.
MigrateProgress(Src:island_progress)<transacts>:island_progress =
    if (Src.Version < CurrentSaveVersion):
        island_progress:
            Version := CurrentSaveVersion
            TownCoins := Src.TownCoins + (CountBadges(Src) * 50)
            MakeIslandProgress<constructor>(Src)
    else:
        Src

# Returns a fresh record with the named zone's badge stamped.
StampBadge(Src:island_progress, Zone:string)<transacts>:island_progress =
    if (Zone = "south-shores"):
        island_progress:
            ShellBadge := true
            MakeIslandProgress<constructor>(Src)
    else if (Zone = "north-jungle"):
        island_progress:
            JungleBadge := true
            MakeIslandProgress<constructor>(Src)
    else if (Zone = "east-volcano"):
        island_progress:
            VolcanoBadge := true
            MakeIslandProgress<constructor>(Src)
    else if (Zone = "west-coves"):
        island_progress:
            CoveBadge := true
            MakeIslandProgress<constructor>(Src)
    else if (Zone = "the-deeps"):
        island_progress:
            DeepsBadge := true
            MakeIslandProgress<constructor>(Src)
    else:
        Src

# ─── Public API (lesson 13's portal_gatekeeper imports these) ─────
EnsureProfile<public>(Player:player)<decides><transacts>:void =
    if (not IslandProgressMap[Player]):
        set IslandProgressMap[Player] = island_progress{Version := CurrentSaveVersion}
    Existing := IslandProgressMap[Player]
    if (Existing.Version < CurrentSaveVersion):
        set IslandProgressMap[Player] = MigrateProgress(Existing)

GrantBadge<public>(Player:player, Zone:string)<decides><transacts>:void =
    EnsureProfile[Player]
    Src := IslandProgressMap[Player]
    set IslandProgressMap[Player] = StampBadge(Src, Zone)

# ─── The Hall of Badges device ────────────────────────────────────
hall_of_badges_device := class(creative_device):

    # The plinth players interact with to inspect their profile.
    @editable
    HallButton : button_device = button_device{}

    # Booth button that stamps THIS island's badge on completion.
    @editable
    GrantButton : button_device = button_device{}

    # Glow that lights once the inspecting player owns any badge.
    @editable
    HallGlow : vfx_spawner_device = vfx_spawner_device{}

    # Professor AweShucks' announcement board.
    @editable
    HallHud : hud_message_device = hud_message_device{}

    # Which zone badge the GrantButton stamps on this island.
    @editable
    ThisZone : string = "south-shores"

    BadgeCountMsg<localizes>(Count:int):message = "Hall of Badges: {Count} / 5 zone badges earned"
    BadgeStampedMsg<localizes>:message = "Badge stamped! Your progress is saved across sessions."

    OnBegin<override>()<suspends>:void =
        Playspace := GetPlayspace()

        # Migrate returning players the moment they arrive.
        Playspace.PlayerAddedEvent().Subscribe(OnPlayerJoined)
        for (ExistingPlayer : Playspace.GetPlayers()):
            if (EnsureProfile[ExistingPlayer]):
                Print("Profile ready (schema v{CurrentSaveVersion})")

        HallButton.InteractedWithEvent.Subscribe(OnHallInspect)
        GrantButton.InteractedWithEvent.Subscribe(OnGrantBadge)

    OnPlayerJoined(NewPlayer:player):void =
        # A returning player's old save is upgraded HERE, before any
        # gate or beacon ever reads it.
        if (EnsureProfile[NewPlayer]):
            Print("Returning save migrated to v{CurrentSaveVersion}")

    OnHallInspect(Agent:agent):void =
        if (Player := player[Agent], Profile := IslandProgressMap[Player]):
            Badges := CountBadges(Profile)
            HallHud.SetText(BadgeCountMsg(Badges))
            HallHud.Show(Agent)
            if (Badges > 0):
                HallGlow.Enable()

    OnGrantBadge(Agent:agent):void =
        if (Player := player[Agent], GrantBadge[Player, ThisZone]):
            HallHud.SetText(BadgeStampedMsg)
            HallHud.Show(Agent)

Line-by-line highlights

Lines What's happening
island_progress := class<final><persistable> The saved record. <final> is required on persistable classes; every field is a persistable type.
Version:int = 0 The migration hook. A brand-new record defaults to 0 and is immediately stamped to CurrentSaveVersion by EnsureProfile.
MakeIslandProgress<constructor>(Src...) Copy-constructor — copies EVERY field. Forget one here and every update silently resets it.
var IslandProgressMap:weak_map(player, island_progress) Module-scope persisted store. The engine saves one entry per player for this island.
MigrateProgress Rebuilds an old-version record: new Version, retroactive coin bonus, everything else copied through the constructor.
EnsureProfile[Player] Failable (<decides>): creates a missing record, upgrades an old one. Called on join and at startup so nothing downstream ever sees a stale schema.
set IslandProgressMap[Player] = island_progress: ... The tycoon idiom: override the changed field(s), then MakeIslandProgress<constructor>(Src) copies the rest.
PlayerAddedEvent().Subscribe(OnPlayerJoined) Returning players are migrated the moment they arrive — before any gate or beacon reads their profile.
if (Player := player[Agent], Profile := IslandProgressMap[Player]) Chained failable bindings: cast the agent, then read the map, all in one guard.

A word about "other islands"

Persistable data is scoped per player, per island: your hub island's IslandProgressMap is saved and restored by the engine every time that player returns to this island, but a different published island keeps its own separate store. The archipelago trick is to make the schema itself the shared part: every zone island in your curriculum ships this same island_progress module, so the code that reads and migrates progress is identical everywhere. When a player finishes a zone capstone and travels back through the hub's matchmaking portal (lesson 16), the hub stamps the badge in its store — and the Hall of Badges lights up for good.

Common patterns

Pattern 1 — The gatekeeper query (a <decides> badge check)

Lesson 13's portal_gatekeeper needs a check that fails when the badge is missing — perfect for gating a conditional button. Minimal standalone version:

using { /Verse.org/Simulation }

zone_profile := class<final><persistable>:
    Version:int = 1
    JungleBadge:logic = false

MakeZoneProfile<constructor>(Src:zone_profile)<transacts> := zone_profile:
    Version := Src.Version
    JungleBadge := Src.JungleBadge

var ZoneProfileMap:weak_map(player, zone_profile) = map{}

# The gatekeeper check lesson 13 imports: succeeds only when the
# player owns the badge — a perfect <decides> gate for a portal.
HasJungleBadge<public>(Player:player)<decides><transacts>:void =
    Profile := ZoneProfileMap[Player]
    Profile.JungleBadge?

Pattern 2 — Evolving the schema safely (v3 adds a field)

Adding a field with a default is save-compatible: an old record simply loads with the default. Removing or re-typing a field is what breaks saves — never do it; version and migrate instead.

Where this goes next

This profile is the spine of the whole center-village arc:

  • Lesson 13 — Badge-Gated Doors imports EnsureProfile / GrantBadge / the badge query into the portal_gatekeeper module: free booths always open, paid-zone booths demand the previous badge.
  • Lesson 15 — The Portal Beacon reads the same profile from a custom scene-graph component to drive the per-player unlock glow.
  • Lesson 18 — Capstone: AweShucks Town shares the IslandProgress schema island-wide: quest rewards deposit TownCoins, capstone returns stamp badges, and the Hall of Badges becomes the town's trophy wall.

Professor AweShucks' parting note: version your saves before you need to. The Version:int = 0 field costs you one line today and saves an island full of returning players tomorrow.

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 How to store per-player progress in a versioned <persistable> record inside a weak_map(player, ...), migrate old saves, and build the IslandProgress badge profile that gates portals across the island 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