An if condition must be able to fail
The single most common compile error when writing Verse from memory. A bare logic value is not a condition.
# ✗ Error: "Expected an expression that can fail in the 'if' condition clause"
if (Hidden):
if (IsActive(Idx)):
# ✓ Compare explicitly
if (Hidden = true):
if (Hidden = false):
if (IsActive(Idx) = true):
Valid failable conditions: comparisons (=, <>, >, <), decides calls written with square brackets, optional unwraps, and set expressions.
set inside if is the normal idiom
Assigning into a map can fail, so it's wrapped and the result discarded. The empty block is not a mistake: it's how you say "I don't care about the failure case".
if (set Balance[Player] = 100) {}
Optionals
var Look : ?post_process_device = false # empty optional
set Look = option{SomeDevice} # filled
if (L := Look?): # unwrap, only runs when present
false as an empty optional reads strangely, but it's correct.
Specifiers you will need
| Specifier | Use |
|---|---|
<suspends> | Function can sleep or await. Required for Sleep, Await, and loop with waits |
<transacts> | Pure-ish query, callable from inside conditions |
<override> | On OnBegin |
<concrete> | On data classes used in @editable arrays |
<final><persistable> | On save-data classes |
Concurrency
spawn { SomeLoop() } # fire and forget, does not block
race: # first one to finish wins, other is cancelled
Sleep(RotateSeconds)
WaitForCollect(Idx, Slot)
loop: # infinite until break
Sleep(1.0)
if (Condition = false):
break
race is the cleanest way to express "whichever happens first": a timer versus a player action, for example.
Iteration
for (Item : Array): # values
for (Idx -> Item : Array): # index and value
Odds and ends
- String interpolation:
"Collected {Amount} coins" Int[SomeFloat]converts and can fail, so it needs unwrapping.Min,Max,Floor,Mod[],GetRandomInt(Low, High)are available.- Billboard text takes a
message, not astring. Keep a smallStringToMessagehelper.
Save data is append-only
A <persistable> class is append-only from the moment the island is first published. Adding a field is safe. Removing or renaming one fails publish validation, because live save data still references it.
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. Treat every persisted field as permanent on the day you add it.
player_save := class<final><persistable>:
Currency : int = 0
Level : int = 0
# 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{}
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:
ErrRuntime_WeakMapInvalidKey: Invalid key used to access persistent
`var` `weak_map`.
SaveLoop(Player : player)<suspends> : void =
loop:
Sleep(SaveIntervalSecs)
if (IsConnected(Player) = false):
break
Save(Player)
IsConnected(Player : player)<transacts> : logic =
for (P : GetPlayspace().GetPlayers()):
if (P = Player):
return true
return false
Full walkthrough: Player progress that survives leaving.
There is no player-name API
Verse cannot read a player's display name. Not through player, not through agent, not through any devices module. GetDisplayName, player_name, Nameplate, EpicAccountId: all return nothing in the digests.
Anything you build in the world that should show who someone is, a name above a head or on a board, can't be built from Verse. Design around it from the start.
There is no timezone or DST handling
GetSecondsSinceEpoch() gives you UTC. Converting a local wall-clock time to that is your problem, and a fixed offset is only correct for half the year.
# Amsterdam: 1 in winter, 2 during summer time.
# No automatic DST -- this gets flipped by hand at each clock change.
TimeZoneOffsetHours : int = 2
The event fires exactly one hour off the time you typed.
Rotating active nodes
Of N total nodes, only a few are awake; each rotates on a timer or on first collection. Full guide: Keep players moving.
loop:
if (Idx := PickSleepingNode()?, Node := Nodes[Idx]):
# ... enable trigger, beacon, start fill loop ...
race:
Sleep(RotateSeconds)
WaitForCollect(Idx, Node)
# ... disable, free the slot ...
else:
Sleep(1.0)
Cue-driven timeline
Model a timed show as an array of <concrete> cue structs, each with a timestamp plus everything that fires at it. Keep the end of the show as its own field. Tying it to the cue list cut a live show off mid-song.
Remaining := ShowDurationSeconds - Elapsed
if (Remaining > 0.0):
Sleep(Remaining)
OnShowEnd()
One-time-per-player gate
Native devices have no per-player use limits. Keep the memory yourself. This lives in memory, not save data.
var HasUsed : [player]logic = map{}
OnEnter(Agent : agent) : void =
if (Player := player[Agent]):
Used := if (U := HasUsed[Player]) then U else false
if (Used = false):
if (set HasUsed[Player] = true) {}
Destination.Teleport(Player)
Pausing a background loop from outside
When one system must suppress another temporarily, a flag checked inside the loop beats trying to kill the loop. Without it, the loop re-enables things a few seconds later and the bug looks random.
var SuppressMarkers : logic = false
SetMarkersHidden(Hidden : logic) : void =
set SuppressMarkers = Hidden
if (Hidden = true):
for (Slot : Slots):
Slot.Beacon.Disable()
else:
for (Idx -> Slot : Slots):
if (IsActive(Idx) = true):
Slot.Beacon.Enable()
# ...and inside the rotation loop:
if (SuppressMarkers = false):
Slot.Beacon.Enable()
Teleporting everyone
TeleportEveryoneTo(Marker : creative_prop) : void =
Where := Marker.GetTransform()
for (Player : GetPlayspace().GetPlayers()):
if (Char := Player.GetFortCharacter[]):
if (Char.TeleportTo[Position := Where.Translation, Rotation := Where.Rotation]) {}
GetFortCharacter[] fails silently for anyone mid-respawn or with a momentary network issue, and that player is skipped with no error. "Some players but not others" isn't a selection, it's timing.