Overview
Every UEFN creative device runs its startup logic in a special method:
OnBegin<override>()<suspends>:void =
See that <suspends> marker? That is a specifier — an effect that tells the Verse compiler this function is allowed to pause its own execution and resume later, potentially many game frames from now. Without it, your code would have to finish in a single instant, and you could never Sleep(2.0) to wait for a dramatic beat, wait for an animation, or run a cooldown.
The game problem it solves: timed, sequenced gameplay. A vault door that opens two seconds after a player arrives on the dock. A welcome sign that fades in after the door. A cooldown before a device can be triggered again. All of these need to wait — and waiting is exactly what <suspends> unlocks.
The key rule is that <suspends> is viral: any function that calls a suspending function (like Sleep) must itself be marked <suspends>. Because OnBegin is already suspending, you can Sleep directly there. But if you factor code into a helper, that helper needs the marker too. That single fact is the source of most 'this won't compile' surprises for beginners, so this whole article is built around it.
We'll drive it home with two real device methods from the grounding: Show() (which reveals a hidden device in the world) and Open(Agent) (which opens a door). Both are instant, but by wrapping them in a suspending sequence we get a timed island moment.
API Reference
(API surface could not be resolved for this device.)
Walkthrough
The scene: a bright cel-shaded cove. A player steps onto a pressure plate on the wooden dock. We wait one second (let them settle), then swing open the cove's driftwood door. After another two-second beat, a hidden welcome sign appears over the shore. Every step of that timing is powered by <suspends>.
We use three placed devices: a trigger_device (the dock plate), a hinged_door_device (whose Open(Agent) method we call), and a conditional_button_device standing in for our hidden sign (whose Show() method reveals it).
# A timed cove sequence: plate -> wait -> open door -> wait -> show sign.
# Every wait is only legal because the running function is <suspends>.
cove_greeter := class(creative_device):
# The dock pressure plate the player steps on.
@editable
DockPlate : trigger_device = trigger_device{}
# The driftwood cove door we swing open.
@editable
CoveDoor : hinged_door_device = hinged_door_device{}
# A welcome sign that starts hidden and appears after the door opens.
@editable
WelcomeSign : conditional_button_device = conditional_button_device{}
# OnBegin is ALREADY <suspends>, so its body may Sleep and wait.
OnBegin<override>()<suspends>:void =
# Subscribe our handler to the plate's TriggeredEvent.
DockPlate.TriggeredEvent.Subscribe(OnPlateStepped)
# A subscribed event handler is a normal method at class scope.
# The event hands us a ?agent; we must unwrap it before using it.
OnPlateStepped(Agent : ?agent):void =
if (Player := Agent?):
# RunGreeting is <suspends>, so we can't just call it inline
# from this NON-suspending handler. We launch it as its own
# async task with `spawn`.
spawn { RunGreeting(Player) }
# This helper WAITS, so it MUST carry the <suspends> marker itself.
# That is the viral rule in action.
RunGreeting(Player : agent)<suspends>:void =
# Let the player settle on the dock for a beat.
Sleep(1.0)
# Open() is instant, but we reached it on our own schedule.
CoveDoor.Open(Player)
# Hold for a two-second dramatic beat before the reveal.
Sleep(2.0)
# Reveal the previously-hidden welcome sign over the shore.
WelcomeSign.Show()
Line by line:
- The three
@editablefields are how Verse gets a handle to devices you placed in the level. You must declare a device as an editable field of aclass(creative_device)— a bareCoveDoor.Open(...)on an undeclared name fails with 'Unknown identifier'. OnBegin<override>()<suspends>:void =is the entry point. Its<suspends>marker is what lets any code path from here eventually wait.- In
OnBeginweSubscribeourOnPlateSteppedmethod to the plate'sTriggeredEvent. Subscribing is done once, at startup. OnPlateStepped(Agent : ?agent)is the handler. Note the parameter is a?agent(an optional). We unwrap it withif (Player := Agent?):— inside thatif,Playeris a realagent.- Here's the crux:
OnPlateSteppedis not marked<suspends>(event handlers generally aren't). So we cannot call our waiting helper directly. Instead we usespawn { RunGreeting(Player) }to launch it as an independent async task.spawnis the bridge from non-suspending code into suspending code. RunGreeting(Player : agent)<suspends>:void =carries the<suspends>marker because it callsSleep. That's the viral rule: calling a suspending function forces you to be suspending too.- Inside,
Sleep(1.0)pauses this task for one second (other game logic keeps running), thenCoveDoor.Open(Player)swings the door, anotherSleep(2.0)beat, thenWelcomeSign.Show()reveals the sign.
Common patterns
Pattern 1 — A cooldown gate using Sleep in a suspending helper
A button on the clifftop opens a door, then goes on a short cooldown so it can't be spammed. The cooldown is a Sleep — only possible because the helper is <suspends>.
cliff_gate := class(creative_device):
@editable
GateButton : button_device = button_device{}
@editable
CliffDoor : hinged_door_device = hinged_door_device{}
var Ready : logic = true
OnBegin<override>()<suspends>:void =
GateButton.InteractedWithEvent.Subscribe(OnPressed)
OnPressed(Agent : agent):void =
if (Ready?):
spawn { OpenWithCooldown(Agent) }
OpenWithCooldown(Agent : agent)<suspends>:void =
set Ready = false
CliffDoor.Open(Agent)
Sleep(5.0) # cooldown window — needs <suspends>
set Ready = true
Pattern 2 — Revealing a hidden device on a delay with Show()
A treasure chest prop starts hidden on the shore. Three seconds after the game begins, it fades into view. The delay lives right in OnBegin, which is already <suspends>.
shore_reveal := class(creative_device):
@editable
HiddenChest : conditional_button_device = conditional_button_device{}
OnBegin<override>()<suspends>:void =
Sleep(3.0) # legal: OnBegin is <suspends>
HiddenChest.Show() # instant reveal, but timed by the wait above
Pattern 3 — Chaining two waits before opening a door
A looping welcome routine: wait, open the cove door for arriving players (passed as agent), wait again, and repeat. loop inside a <suspends> context relies on suspension points like Sleep to yield each frame.
welcome_loop := class(creative_device):
@editable
Entrance : hinged_door_device = hinged_door_device{}
@editable
StartPlate : trigger_device = trigger_device{}
OnBegin<override>()<suspends>:void =
StartPlate.TriggeredEvent.Subscribe(OnArrive)
OnArrive(Agent : ?agent):void =
if (Player := Agent?):
spawn { StaggeredOpen(Player) }
StaggeredOpen(Player : agent)<suspends>:void =
Sleep(0.5)
Sleep(0.5) # two short beats
Entrance.Open(Player)
Gotchas
- The viral rule bites factored code.
Sleep,Await, and animation waits are<suspends>. The moment you call one, your function needs<suspends>too — and so does its caller, all the way up.OnBeginis already suspending, so top-level waits are free; helpers you write are not. - Event handlers are usually NOT suspending.
TriggeredEvent/InteractedWithEventhandlers run synchronously, so you can'tSleepdirectly inside them. Bridge into async work withspawn { MyHelper(...) }, whereMyHelperis<suspends>. <suspends>and<decides>cannot be combined on the same function. A function is either the pause-and-resume kind or the succeed/fail kind, not both.?agentmust be unwrapped. Alistenable(?agent)event hands you a?agent. Useif (Player := Agent?):before callingOpen(Player)—Opentakes a plainagent, not an optional.Sleep(0.0)still yields a frame. Even a zero-second sleep is a suspension point; it lets other tasks run and lets cancellation be checked. Waiting is cooperative, never a hard freeze.Show()andOpen()are instant.<suspends>doesn't make them slow — it makes the surrounding timing possible. The device action still happens in one frame; the drama comes from theSleeps around it.