Overview
Verse is strict about structure: a code block (indented body) must always follow an identifier like if, for, loop, a function name, or a class definition. When you need to group a sequence of expressions mid-function — perhaps to limit a variable's lifetime, clarify a logical phase, or simply keep related steps together — you reach for the block expression.
A block expression:
- Introduces a new nested scope. Variables declared inside it cannot be referenced after the closing block.
- Returns a value — the result of its last expression (just like a regular code block).
- Can appear anywhere an expression is valid, including inside
ifbranches, event handlers, orOnBegin. - Has no runtime cost — it is purely a scoping/readability construct.
When to reach for it:
- You want to reuse a short variable name (like
ResultorTemp) in two separate phases without collision. - You want to visually separate "setup", "action", and "teardown" phases inside a single function.
- You need the compiler to enforce that a temporary value isn't accidentally used later.
A block is NOT a loop, NOT a conditional, and NOT a coroutine boundary. It simply groups expressions and scopes their variables.
API Reference
(API surface could not be resolved for this device.)
Walkthrough
Scenario: A box-fight arena manager. When the round starts, two outer barriers go up and a mid-lane barrier goes down. A countdown timer runs. When time expires (SuccessEvent fires), the mid barrier re-enables and the outer ones drop — locking players in their lanes for the final showdown. Each phase is wrapped in a block so its local variables stay scoped to that phase.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
box_fight_manager := class<concrete>(creative_device):
# Wire these up in the UEFN editor
@editable
Barrier1 : barrier_device = barrier_device{}
@editable
Barrier2 : barrier_device = barrier_device{}
@editable
MidBarrier : barrier_device = barrier_device{}
@editable
RoundTimer : timer_device = timer_device{}
# Called when the timer completes successfully (countdown hits zero)
OnRoundEnd(MaybeAgent : ?agent) : void =
# --- Phase 1: lock outer lanes ---
block:
# These variables only exist inside this block
OuterA := Barrier1
OuterB := Barrier2
OuterA.Enable()
OuterB.Enable()
# OuterA and OuterB are gone here — no accidental reuse
# --- Phase 2: open mid-lane ---
block:
Mid := MidBarrier
Mid.Disable()
# Mid is gone here
OnBegin<override>()<suspends> : void =
# --- Setup phase: open outer lanes, seal mid ---
block:
Barrier1.Disable()
Barrier2.Disable()
MidBarrier.Enable()
# Subscribe to the timer's success event
RoundTimer.SuccessEvent.Subscribe(OnRoundEnd)
# Start the round timer (no specific agent — global timer)
RoundTimer.Enable()
Line-by-line explanation:
| Lines | What's happening |
|---|---|
@editable fields |
Three barrier_devices and a timer_device wired in the editor. |
OnRoundEnd handler |
Subscribed to SuccessEvent; receives ?agent (may be none). |
First block: in OnRoundEnd |
Scopes OuterA/OuterB aliases — enables both outer barriers. After the block closes, those names are gone. |
Second block: in OnRoundEnd |
Scopes Mid — disables the mid barrier. Clean separation of intent. |
block: in OnBegin |
Groups the three setup calls into a named phase visually, even though no new variables are declared — purely for readability. |
RoundTimer.SuccessEvent.Subscribe(OnRoundEnd) |
Wires the timer's completion event to our handler. |
RoundTimer.Enable() |
Starts the timer device. |
Common patterns
Pattern 1 — Scoped temporary variable that must not leak
You want to compute a "phase label" string and use it in exactly one place. Wrapping it in a block guarantees it can't be accidentally referenced later.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
scoped_var_demo := class<concrete>(creative_device):
@editable
Barrier1 : barrier_device = barrier_device{}
@editable
Barrier2 : barrier_device = barrier_device{}
OnBegin<override>()<suspends> : void =
# Phase A — enable both barriers, track count in a scoped var
block:
EnabledCount : int = 2
Barrier1.Enable()
Barrier2.Enable()
# EnabledCount is valid here
if (EnabledCount > 1):
MidBarrier.Disable()
# EnabledCount does NOT exist here — compiler enforces it
# Phase B — later logic is clearly separate
block:
Barrier1.Disable()
Barrier2.Disable()
@editable
MidBarrier : barrier_device = barrier_device{}
Pattern 2 — Using block as an expression that returns a value
block evaluates to the result of its last expression. You can assign that result to a variable in the outer scope.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
block_as_expression_demo := class<concrete>(creative_device):
@editable
MainBarrier : barrier_device = barrier_device{}
@editable
RoundTimer : timer_device = timer_device{}
# Returns true if setup succeeded (last expression in block)
SetupArena() : void =
# The block groups setup; its last call is Enable()
block:
MainBarrier.Disable() # open the arena
RoundTimer.Enable() # start the clock
# last expression — block "returns" void here, matching SetupArena's return
OnBegin<override>()<suspends> : void =
SetupArena()
Pattern 3 — Ignore-list management scoped per phase
Add a VIP agent to the barrier's ignore list during a setup window, then remove all ignored agents when the round locks. Each operation is wrapped in its own block to make the intent crystal-clear.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
vip_barrier_demo := class<concrete>(creative_device):
@editable
GateBarrier : barrier_device = barrier_device{}
@editable
RoundTimer : timer_device = timer_device{}
# Call this when a VIP player needs to pass through
GrantVIPAccess(VIPAgent : agent) : void =
block:
# Only this block cares about VIPAgent in the ignore list
GateBarrier.AddToIgnoreList(VIPAgent)
# VIPAgent reference is still valid outside, but the
# intent is clearly scoped to this operation
OnRoundLock(MaybeAgent : ?agent) : void =
block:
# Lock-down phase: clear ALL ignore exceptions
GateBarrier.RemoveAllFromIgnoreList()
GateBarrier.Enable()
OnBegin<override>()<suspends> : void =
RoundTimer.SuccessEvent.Subscribe(OnRoundLock)
RoundTimer.Enable()
Gotchas
1. block is NOT optional syntax sugar — it's required when you need a nested scope mid-expression
You cannot just open an indented block without a preceding identifier. If you try:
# WRONG — Verse won't accept a bare indented block
OnBegin<override>()<suspends> : void =
Barrier1.Enable()
Barrier2.Enable() # syntax error — unexpected indent
Use block: to introduce that nested body legitimately.
2. Variables declared inside block are invisible outside it
This is a feature, not a bug — but it surprises newcomers:
block:
X : int = 5
# X is unknown here — compiler error if you reference it
If you need a value after the block, declare it before the block, or capture the block's return value.
3. block returns the value of its LAST expression
If your last expression is void (like Enable()), the block is void. If you want to return a computed value, make sure the last line is the expression you care about — not a side-effecting call.
4. block does NOT create a new coroutine or suspension point
A block inside a <suspends> function inherits that context, but the block keyword itself does not add any concurrency. Don't confuse it with spawn or race.
5. Indentation must be consistent
Verse uses indentation to determine block membership. All expressions inside a block: must be indented exactly one level deeper than the block keyword itself. Mixed tabs/spaces will cause cryptic parse errors.
6. ?agent events must be unwrapped before use
When subscribing to events like timer_device.SuccessEvent (which sends ?agent), your handler receives MaybeAgent : ?agent. Unwrap it before calling agent-specific APIs:
OnRoundEnd(MaybeAgent : ?agent) : void =
if (A := MaybeAgent?):
block:
# A is a real agent here
GateBarrier.AddToIgnoreList(A)