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:
- The current task freezes at that line.
- Verse hands control back to the runtime.
- 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.