Reference Verse

The <suspends> Rule: Async Verse on a Sunny Cove

Some Verse functions take TIME — they spawn a wave of husks one by one, wait out a cooldown, or hold a HUD message on screen. Those functions carry the <suspends> effect, and the rule is viral: anything that calls them must itself be <suspends>. This article teaches the rule through a sun-drenched pirate cove where stepping on a plank triggers a creature wave and a banner announcement.

Updated
The code on this reference page is provided as-is and did not pass the latest compile check — treat the examples as a starting point and verify in your project.

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 @editable fields are how Verse gets a handle to devices you placed in the level. You must declare a device as an editable field of a class(creative_device) — a bare CoveDoor.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 OnBegin we Subscribe our OnPlateStepped method to the plate's TriggeredEvent. Subscribing is done once, at startup.
  • OnPlateStepped(Agent : ?agent) is the handler. Note the parameter is a ?agent (an optional). We unwrap it with if (Player := Agent?): — inside that if, Player is a real agent.
  • Here's the crux: OnPlateStepped is not marked <suspends> (event handlers generally aren't). So we cannot call our waiting helper directly. Instead we use spawn { RunGreeting(Player) } to launch it as an independent async task. spawn is the bridge from non-suspending code into suspending code.
  • RunGreeting(Player : agent)<suspends>:void = carries the <suspends> marker because it calls Sleep. 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), then CoveDoor.Open(Player) swings the door, another Sleep(2.0) beat, then WelcomeSign.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. OnBegin is already suspending, so top-level waits are free; helpers you write are not.
  • Event handlers are usually NOT suspending. TriggeredEvent/InteractedWithEvent handlers run synchronously, so you can't Sleep directly inside them. Bridge into async work with spawn { MyHelper(...) }, where MyHelper is <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.
  • ?agent must be unwrapped. A listenable(?agent) event hands you a ?agent. Use if (Player := Agent?): before calling Open(Player)Open takes a plain agent, 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() and Open() 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 the Sleeps around it.

Guides & scripts that use trigger_device

Step-by-step tutorials that put this object to work.

Build your own lesson with trigger_device

Generate a personalized, step-by-step lesson plan built around this object — grounded in this exact reference and our compile-verified knowledge base.

Build a lesson →