The Hub Heartbeat: Custom Round Logic in Verse

In this lesson you'll learn to

  • βœ“Student can orchestrate a start/play/win/reset game loop in Verse that coordinates score, timer, and spawns.

Student can orchestrate a start/play/win/reset game loop in Verse that coordinates score, timer, and spawns.

πŸ“– Reference & full walkthrough: The Hub Heartbeat: Custom Round Logic in Verse

πŸ” Builds on: south-shores: enum-with-data-pattern game-phase state machine module

🧩 Your capstone piece: The hub lobby loop: WAITING -> READY -> LAUNCH -> RESET state machine


What you'll learn

  • Session-scoped weak_map: how to store per-player data that survives a round reset by keying it on GetSession().
  • The logic type as a flag: using true/false to mark which players are handicapped, tested with if (Flag?):.
  • Reading persisted data in OnBegin: OnBegin runs at the start of every round, so it's the place to check the flag and react.
  • Real player-UI calls: GetPlayerUI[], building a canvas with a text_block, and AddWidget to actually show the handicap on screen.

How it works

The round-reset trap

When a round ends, most state is wiped. A plain var on your device won't reliably survive into the next round. The fix is a weak_map keyed on the current session.

Session-scoped weak maps

GetSession() (from /Verse.org/Simulation) returns the session for the current match. The docs note its result "can be used with weak_map to implement global variables." We declare a module-level weak_map(session, loser_table) β€” a map from a session to a tiny loser_table class that holds a [player]logic flag map. (The value type of a weak_map must be a named type β€” writing the map type [player]logic inline inside weak_map(...) doesn't parse β€” so we wrap it in a one-field class.) Because it's keyed on the session, the same entry is visible across rounds within the same match.

Fallible map access is the key idea

LoserFlags[Session] and Table.Flags[Player] are fallible index operations β€” they use [] and MUST live in a failure context (an if/for condition or a <decides> body). They fail (rather than crash) when no entry exists yet. That's why IsLoser is marked <decides>: it simply states the two lookups, and failure means "this player isn't flagged."

The flow

  1. OnBegin runs at round start. For every currently-flagged player, we push a HUD widget warning them they're handicapped.
  2. A trigger ("the loser buzzer") lets you flag a player as the loser for next round β€” it writes true into the map.
  3. Because the data is session-scoped, the next round's OnBegin sees the flag and shows the UI.

Why logic, not bool

Verse's boolean type is logic with values true/false. You test it with if (Flag?):. There is no bool, and you never write = true β€” you interrogate the value with ?.

Let's build it

Drag one trigger_device into your island and link it below. Step on it (or wire a button to it) to mark the last player who touched it as "the loser" for the next round. On the next round's start, that player gets a real HUD widget warning.

using { /Fortnite.com/Devices }
using { /Fortnite.com/Characters }
using { /Fortnite.com/UI }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/UI }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /UnrealEngine.com/Temporary/Diagnostics }

# weak_map's value must be a NAMED type β€” an inline [player]logic map type
# won't parse inside weak_map(...) β€” so we wrap the flag map in a tiny class.
loser_table := class:
    Flags : [player]logic = map{}

# Session-scoped storage: for each session (round chain), a per-player logic flag.
# Keyed on session so the flags SURVIVE a round reset within the same match.
var LoserFlags : weak_map(session, loser_table) = map{}

round_handicap_manager := class<concrete>(creative_device):

    # Step on this (or wire a button to it) to flag the toucher as next round's loser.
    @editable
    LoserBuzzer : trigger_device = trigger_device{}

    # Localized text helper β€” message fields never take raw strings.
    HandicapText<localizes>(S : string) : message = "{S}"

    # OnBegin runs at the START of every round -> perfect place to react to the flag.
    OnBegin<override>()<suspends>: void =
        # Show the handicap UI to anyone already flagged from a previous round.
        for (Player : GetPlayspace().GetPlayers(), IsLoser[Player]):
            ShowHandicapUI(Player)
        # Listen for new losers being flagged this round.
        LoserBuzzer.TriggeredEvent.Subscribe(OnBuzzed)

    # Fallible: succeeds only when the player has a flag set to true.
    # Both [] lookups are fallible index ops, legal inside this <decides> body.
    IsLoser(Player : player)<decides><transacts>: void =
        Table := LoserFlags[GetSession()]  # fails if no table for this session yet
        Flag := Table.Flags[Player]        # fails if player not present
        Flag?                              # fails if the stored logic is false

    # Trigger handler: mark the triggering agent as the loser for next round.
    OnBuzzed(MaybeAgent : ?agent): void =
        if (Agent := MaybeAgent?, Player := player[Agent]):
            Session := GetSession()
            # Read the existing flag map for this session (or start a fresh one).
            var Flags : [player]logic = map{}
            if (Existing := LoserFlags[Session]):
                set Flags = Existing.Flags
            # Add this player as true, then write the table back to the session.
            if (set Flags[Player] = true, set LoserFlags[Session] = loser_table{Flags := Flags}):
                # Give immediate feedback to the player who just got flagged.
                ShowHandicapUI(Player)

    # REAL player-UI: build a canvas with a warning text_block and add it to the HUD.
    ShowHandicapUI(Player : player): void =
        if (PlayerUI := GetPlayerUI[Player]):
            Warning := text_block:
                DefaultText := HandicapText("HANDICAP: you lost last round!")
            Canvas := canvas:
                Slots := array:
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.1}, Maximum := vector2{X := 0.5, Y := 0.1}}
                        Offsets := margin{Top := 0.0, Left := 0.0}
                        Alignment := vector2{X := 0.5, Y := 0.0}
                        Widget := Warning
            PlayerUI.AddWidget(Canvas)
            Print("Showed handicap UI to a flagged player.")

Try it yourself

  • Multi-round test: Set Total Rounds to 2+ in Island Settings. Step on the buzzer in Round 1, then watch the HUD warning appear at the start of Round 2.
  • Clear the flag: Add a second trigger whose handler sets set Flags[Player] = false (then writes the map back) so a player can "redeem" themselves.
  • Real handicap: Instead of just text, apply a gameplay effect in ShowHandicapUI β€” reduce a stat, disable a class, or spawn near the back of the line.
  • Reset on game end: Because there's no API for the current round, add an @editable TotalRounds : int and clear LoserFlags when the match's final round begins.

Recap

You stored a per-player logic flag in a session-keyed weak_map so it persists across rounds within a match. OnBegin reads that flag at the start of each round via a <decides> helper (the [] lookups are fallible index ops that fail cleanly when there's no entry), and for every flagged player you built a real canvas + text_block and called GetPlayerUI[] / AddWidget to push a live HUD warning. Remember: [] means fallible access inside a failure context, () means an ordinary call β€” and Verse booleans are logic, tested with ?.

πŸ”’

Keep going β€” free

You've read the intro. The rest of this lesson is free for members. Sign in to continue and track your progress.

Sign in free to continue this lesson
Pass the quiz above to chart this quest in your Journal.

Sources

/guides/custom-round-logic-using-verse
Source

Biloxi Studios original lesson

Guided course

Add this lesson to your free study plan.

🧭 The Keeper’s log

Quest complete? Chart your next heading from the 🧠 Center Village (Free) expedition.

β›΅ Take me there