Reference Verse compiles

Awaiting Events in Verse: Suspending Until Something Happens

Every great game moment is a reaction: the door opens *when* the player steps on the plate, the boss music starts *when* the creature spawns, the reward drops *when* the crate lands. Verse's `Await` mechanism lets you write that logic in a straight line — suspend the task, wait for the event, then carry on — instead of wiring a tangle of callbacks. This article teaches you exactly how to await device events, unwrap optional agents, and loop for repeating triggers.

Updated Examples verified on the live UEFN compiler
Watch the Knotawait_an_event in ~90 seconds.

Overview

Verse is a concurrent language. Every creative_device can spawn independent tasks with spawn, and those tasks can suspend themselves by calling Await() on any listenable. A listenable is the read-only face of an event: you can wait on it but you cannot signal it from the outside. Every UEFN creative device exposes its state changes as listenable fields — TriggeredEvent, SpawnedEvent, LandingEvent, and so on.

When you call MyEvent.Await() inside a <suspends> function:

  1. The current task freezes at that line.
  2. Verse hands control back to the runtime.
  3. The moment the device fires the event, your task wakes up and receives the payload.

This pattern replaces polling loops and makes your intent crystal-clear. Use it whenever you need to react to a single device moment; wrap it in a loop when you need to react every time.

When to reach for Await:

  • Sequencing: do A, wait for B, then do C.
  • One-shot reactions: unlock a vault after a creature is eliminated.
  • Repeating reactions: respawn a supply drop every time the previous one is opened.

API Reference

(API surface could not be resolved for this device.)

Walkthrough

Scenario: A boss arena. When the round starts, a creature_placer_device spawns a boss creature. The moment the boss is eliminated, a supply_drop_spawner_device drops a reward crate from the sky. When the crate lands, a trigger_device is triggered to open a vault door (wired in the editor). We await all three events in sequence — no callbacks, no polling.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Verse }

boss_arena_manager := class(creative_device):

    # Wire these in the UEFN editor via the @editable panel.
    @editable
    BossPlacer : creature_placer_device = creature_placer_device{}

    @editable
    RewardDrop : supply_drop_spawner_device = supply_drop_spawner_device{}

    @editable
    VaultTrigger : trigger_device = trigger_device{}

    # Entry point — runs when the island starts.
    OnBegin<override>()<suspends> : void =
        # Step 1 — spawn the boss and wait until it actually appears.
        BossPlacer.Spawn()

        # SpawnedEvent sends the agent (the creature) — no option unwrap needed.
        _BossAgent := BossPlacer.SpawnedEvent.Await()

        Print("Boss has entered the arena!")

        # Step 2 — wait for the boss to be eliminated.
        # EliminatedEvent sends ?agent (the killer — false if non-agent killed it).
        MaybeKiller := BossPlacer.EliminatedEvent.Await()

        if (Killer := MaybeKiller?):
            Print("Boss eliminated by a player!")
        else:
            Print("Boss eliminated by the environment.")

        # Step 3 — spawn the reward supply drop (no agent needed for this overload).
        RewardDrop.Spawn()

        # Step 4 — wait for the crate to touch down.
        # LandingEvent sends tuple() — just a signal, no payload to capture.
        RewardDrop.LandingEvent.Await()

        Print("Reward crate has landed — opening the vault!")

        # Step 5 — fire the vault trigger (wired to a door in the editor).
        VaultTrigger.Trigger()

Line-by-line explanation:

Lines What's happening
@editable fields Declare the three devices so UEFN can wire them to placed actors. Without @editable the identifiers are unknown at runtime.
BossPlacer.Spawn() Tells the creature placer to start the spawn process (non-suspending).
BossPlacer.SpawnedEvent.Await() Suspends until the creature actually appears in the world; receives the agent handle.
BossPlacer.EliminatedEvent.Await() Suspends until the creature dies; receives ?agent (the killer or false).
if (Killer := MaybeKiller?) Unwraps the option — this is required because the payload is ?agent, not agent.
RewardDrop.Spawn() Spawns the supply drop with no owning agent.
RewardDrop.LandingEvent.Await() Suspends until the crate hits the ground; payload is tuple() so we discard it.
VaultTrigger.Trigger() Fires the trigger device — any editor-wired devices (e.g. a door) respond immediately.

Common patterns

Pattern 1 — Looping trigger: respawn supply drops indefinitely

Every time a player opens the crate, destroy it and spawn a fresh one. Use a loop so the Await repeats.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Verse }

endless_supply_manager := class(creative_device):

    @editable
    RewardDrop : supply_drop_spawner_device = supply_drop_spawner_device{}

    OnBegin<override>()<suspends> : void =
        # Spawn the first drop immediately.
        RewardDrop.Spawn()

        loop:
            # Wait for any agent to open the crate.
            Opener := RewardDrop.OpenedEvent.Await()
            # OpenedEvent sends agent (not ?agent) — no unwrap needed.
            Print("Crate opened! Cleaning up and respawning...")

            # Tear down the old drop and spawn a new one.
            RewardDrop.DestroySpawnedDrops()
            Sleep(3.0)   # brief pause before the next drop appears
            RewardDrop.Spawn()

Key point: OpenedEvent sends a plain agent, not ?agent, so you can use the value directly without an option-unwrap.


Pattern 2 — Awaiting a trigger with optional-agent unwrap

A pressure plate (trigger_device) can be activated by a player or by code. The payload is ?agent. This pattern safely handles both cases.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Verse }

plate_watcher := class(creative_device):

    @editable
    Plate : trigger_device = trigger_device{}

    @editable
    RewardDrop : supply_drop_spawner_device = supply_drop_spawner_device{}

    OnBegin<override>()<suspends> : void =
        loop:
            # TriggeredEvent on trigger_device sends ?agent.
            MaybeAgent := Plate.TriggeredEvent.Await()

            if (SteppingAgent := MaybeAgent?):
                # A real player stepped on the plate — spawn a drop for their team.
                RewardDrop.Spawn(SteppingAgent)
                Print("Player triggered the plate — supply drop incoming!")
            else:
                # Triggered by code or a non-agent — spawn a neutral drop.
                RewardDrop.Spawn()
                Print("Plate triggered without a player.")

Key point: trigger_device.TriggeredEvent sends ?agent. Always unwrap with if (A := MaybeAgent?) before passing the agent to APIs that require a concrete agent.


Pattern 3 — Parallel awaits with race

Sometimes you want to react to whichever event fires first — for example, either the balloon is popped OR the crate is destroyed, and you want to clean up in either case.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Verse }

supply_race_manager := class(creative_device):

    @editable
    RewardDrop : supply_drop_spawner_device = supply_drop_spawner_device{}

    @editable
    VaultTrigger : trigger_device = trigger_device{}

    OnBegin<override>()<suspends> : void =
        RewardDrop.Spawn()

        # race suspends until ONE branch completes, then cancels the other.
        race:
            # Branch A: balloon popped (sends ?agent)
            block:
                _MaybePopper := RewardDrop.BalloonPoppedEvent.Await()
                Print("Balloon popped — crate freefalling!")

            # Branch B: crate destroyed before landing (sends ?agent)
            block:
                _MaybeDestroyer := RewardDrop.DestroyCrateEvent.Await()
                Print("Crate was destroyed mid-air!")

        # Whichever happened first, open the vault.
        VaultTrigger.Trigger()
        Print("Vault opened regardless of outcome.")

Key point: race is the right tool when multiple events could resolve the same situation. The winning branch's result is used; the losing branch is cancelled automatically.

Gotchas

1. ?agent vs agent — always check the payload type

Many device events send ?agent (an option), not a plain agent. If you try to pass a ?agent directly to a function that expects agent, the compiler rejects it. Always unwrap:

# WRONG — compiler error if EliminatedEvent sends ?agent
SomeFunction(BossPlacer.EliminatedEvent.Await())

# RIGHT
MaybeKiller := BossPlacer.EliminatedEvent.Await()
if (Killer := MaybeKiller?):
    SomeFunction(Killer)

2. Await only fires ONCE per call — use loop for repeating reactions

A bare Await() suspends, wakes once, and then the function continues past it. If you want to react every time the event fires, wrap the Await in a loop:

# Reacts to every trigger, not just the first
loop:
    _ := Plate.TriggeredEvent.Await()
    DoSomething()

3. Await requires a <suspends> context

You can only call Await() inside a function marked <suspends>. If you try to call it in a plain (non-suspending) function you'll get a compile error. OnBegin<override>()<suspends> is already suspending, and any helper you extract must also be tagged <suspends>.

4. @editable is mandatory for placed devices

A device reference that is NOT declared as an @editable field inside your class(creative_device) will be an unknown identifier at runtime. You cannot construct a live device with creature_placer_device{} — that creates a blank stub. Always wire real placed devices through the UEFN editor panel.

5. LandingEvent payload is tuple() — discard it

supply_drop_spawner_device.LandingEvent is typed listenable(tuple()). The payload carries no useful data. Assign it to _ or just call .Await() and ignore the return:

RewardDrop.LandingEvent.Await()  # payload discarded — that's fine

6. Sleep() takes a float, not an int

Verse does not auto-convert int to float. Write Sleep(3.0), not Sleep(3), or you'll get a type-mismatch compile error.

Build your own lesson with await_an_event

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 →