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 onGetSession(). - The
logictype as a flag: usingtrue/falseto mark which players are handicapped, tested withif (Flag?):. - Reading persisted data in
OnBegin:OnBeginruns at the start of every round, so it's the place to check the flag and react. - Real player-UI calls:
GetPlayerUI[], building acanvaswith atext_block, andAddWidgetto 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
OnBeginruns at round start. For every currently-flagged player, we push a HUD widget warning them they're handicapped.- A trigger ("the loser buzzer") lets you flag a player as the loser for next round β it writes
trueinto the map. - Because the data is session-scoped, the next round's
OnBeginsees 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 : intand clearLoserFlagswhen 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 lessonSources
/guides/custom-round-logic-using-verseBiloxi Studios original lesson
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