Overview
In Verse, an event is a signal that something happened in the game world. Devices expose events as listenable values (e.g. TriggeredEvent, InteractedWithEvent, ZoneOccupiedEvent). You connect your own function to one of these events by calling Subscribe, which returns a cancelable token. Hold onto that token — calling Cancel() on it later stops your handler from receiving any more signals.
This subscribe/unsubscribe cycle solves a very common island-design problem: you want logic to run only while a certain condition is active. For example, a clifftop watchtower that tracks every player who enters its detection zone, but stops tracking the moment the round ends. Subscribe when the round starts, cancel when it ends — clean and precise.
Reach for this pattern whenever:
- Multiple independent systems need to react to the same game event.
- You need to turn a behaviour on and off at runtime.
- You want to avoid polling (checking state every tick) in favour of reactive, event-driven code.
API Reference
(API surface could not be resolved for this device.)
Walkthrough
Scenario: The Clifftop Cove Checkpoint
Your island has a sun-drenched cove with a clifftop trigger zone. When a player enters the zone they earn a point; when the round ends the tracker unsubscribes so no late arrivals are counted. A second trigger on the dock resets the score board and re-subscribes the zone listener, ready for the next round.
This example shows:
- Subscribing to a
ZoneOccupiedEvent(alistenable(agent)) and storing thecancelable. - Unwrapping the optional agent payload.
- Cancelling the subscription when the round ends.
- Re-subscribing when a new round begins from the dock trigger.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# Clifftop Cove Checkpoint — subscribe / unsubscribe demo
clifftop_checkpoint_device := class(creative_device):
# Drop a capture_area_device over the clifftop zone in your level
@editable ClifftopZone : capture_area_device = capture_area_device{}
# A trigger_device on the dock resets and reopens the checkpoint
@editable DockResetTrigger : trigger_device = trigger_device{}
# A trigger_device that fires when the round ends (e.g. wired to a round manager)
@editable RoundEndTrigger : trigger_device = trigger_device{}
# A score manager to award points
@editable ScoreBoard : score_manager_device = score_manager_device{}
# Store the cancelable so we can unsubscribe later
var ZoneSubscription : ?cancelable = false
OnBegin<override>()<suspends> : void =
# Subscribe to the dock reset trigger — handler is a class method
DockResetTrigger.TriggeredEvent.Subscribe(OnDockReset)
# Subscribe to the round-end trigger
RoundEndTrigger.TriggeredEvent.Subscribe(OnRoundEnd)
# Open the checkpoint immediately at island start
OpenCheckpoint()
# Called when the dock reset trigger fires
OnDockReset(Agent : ?agent) : void =
# Re-open the checkpoint for the next round
OpenCheckpoint()
# Called when the round-end trigger fires
OnRoundEnd(Agent : ?agent) : void =
CloseCheckpoint()
# Subscribe to the clifftop zone and store the cancelable
OpenCheckpoint() : void =
# Only subscribe if we are not already subscribed
if (ZoneSubscription?):
# Already subscribed
else:
set ZoneSubscription = option{ ClifftopZone.AgentEntersEvent.Subscribe(OnPlayerEnterZone) }
# Cancel the subscription — no more points awarded
CloseCheckpoint() : void =
if (Sub := ZoneSubscription?):
Sub.Cancel()
set ZoneSubscription = false
# Handler for AgentEntersEvent — payload is `agent` (not optional here)
OnPlayerEnterZone(EnteredAgent : agent) : void =
# Award a point to whoever stepped into the clifftop zone
ScoreBoard.Activate(EnteredAgent)```
**Line-by-line highlights:**
| Line | What it teaches |
|---|---|
| `@editable ClifftopZone` | Every device you call must be declared as an `@editable` field — you cannot reference a placed device any other way. |
| `var ZoneSubscription : ?cancelable = false` | Store the `cancelable` in an optional so you can check whether you are currently subscribed. |
| `.Subscribe(OnDockReset)` | Pass a *method reference* (no parentheses, no arguments) — Verse wires the signature automatically. |
| `OnDockReset(Agent : ?agent)` | `TriggeredEvent` on a `trigger_device` sends `?agent`; the handler signature must match exactly. |
| `ZoneOccupiedEvent.Subscribe(OnPlayerEnterZone)` | `ZoneOccupiedEvent` sends a plain `agent` (not optional), so the handler takes `agent` directly. |
| `Sub.Cancel()` | Calling `Cancel()` on the stored `cancelable` stops the handler immediately. |
| `set ZoneSubscription = false` | Reset to `false` so `OpenCheckpoint` knows it is safe to re-subscribe. |
## Common patterns
### Pattern 1 — One-shot subscription (fire once, then cancel itself)
A seagull sound plays the first time any player reaches the shore dock, then never again.
```verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# Plays a one-shot effect the first time the dock trigger fires
dock_first_arrival_device := class(creative_device):
@editable DockTrigger : trigger_device = trigger_device{}
@editable AmbientSpeaker : audio_player_device = audio_player_device{}
# We need a mutable slot to hold the cancelable
var ArrivalSub : ?cancelable = false
OnBegin<override>()<suspends> : void =
set ArrivalSub = option{ DockTrigger.TriggeredEvent.Subscribe(OnFirstArrival) }
OnFirstArrival(Agent : ?agent) : void =
# Play the ambient sound once
if (A := Agent?):
AmbientSpeaker.Play()
# Immediately cancel — this handler will never fire again
if (Sub := ArrivalSub?):
Sub.Cancel()
set ArrivalSub = false
Key idea: The handler cancels its own subscription on first invocation — a self-removing one-shot listener.
Pattern 2 — Multiple independent subscribers to the same event
Both the score board and a visual effect device react to the same clifftop zone entry, registered independently.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# Two independent handlers wired to one event
multi_subscriber_device := class(creative_device):
@editable ClifftopZone : capture_area_device = capture_area_device{}
@editable ScoreBoard : score_manager_device = score_manager_device{}
@editable VFXTrigger : trigger_device = trigger_device{}
# Store both cancelables so either can be cancelled independently
var ScoreSub : ?cancelable = false
var VFXSub : ?cancelable = false
OnBegin<override>()<suspends> : void =
set ScoreSub = option{ ClifftopZone.ZoneOccupiedEvent.Subscribe(OnZoneScore) }
set VFXSub = option{ ClifftopZone.ZoneOccupiedEvent.Subscribe(OnZoneVFX) }
# Handler 1 — awards a point
OnZoneScore(EnteredAgent : agent) : void =
ScoreBoard.Activate(EnteredAgent)
# Handler 2 — fires a visual trigger (e.g. confetti burst on the clifftop)
OnZoneVFX(EnteredAgent : agent) : void =
VFXTrigger.Trigger()
# You can cancel just the VFX without touching the score
StopVFX() : void =
if (Sub := VFXSub?):
Sub.Cancel()
set VFXSub = false
Key idea: Each Subscribe call returns its own independent cancelable. You can cancel them separately — the score handler keeps running even after StopVFX() is called.
Pattern 3 — Subscribing inside a spawned async task
Sometimes you want to start listening only after an async condition is met (e.g. a countdown finishes). Spawn a task, await the condition, then subscribe.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# Waits for a countdown trigger, then opens the cove zone
countdown_then_open_device := class(creative_device):
@editable CountdownTrigger : trigger_device = trigger_device{}
@editable CoveZone : capture_area_device = capture_area_device{}
@editable ScoreBoard : score_manager_device = score_manager_device{}
var CoveSub : ?cancelable = false
OnBegin<override>()<suspends> : void =
# Spawn a background task that waits for the countdown
spawn{ WaitThenOpen() }
WaitThenOpen()<suspends> : void =
# Block until the countdown trigger fires
CountdownTrigger.TriggeredEvent.Await()
# Now subscribe — the cove is open!
set CoveSub = option{ CoveZone.ZoneOccupiedEvent.Subscribe(OnCoveEntered) }
OnCoveEntered(EnteredAgent : agent) : void =
ScoreBoard.Activate(EnteredAgent)
Key idea: Await() and Subscribe() are complementary tools on the same listenable. Use Await() inside a <suspends> function when you want to pause until one signal; use Subscribe() when you want to react to every signal.
Gotchas
1. You MUST store the cancelable — or the GC will eat your subscription
Subscribe returns a cancelable. If you discard it (assign to _ or ignore the return value), the garbage collector may eventually destroy the subscription object and your handler silently stops firing. Always store it in a var field on your creative_device.
# ❌ WRONG — subscription may be GC'd at any time
MyZone.ZoneOccupiedEvent.Subscribe(OnEnter)
# ✅ CORRECT — stored on the device, lives as long as the device
set MySub = option{ MyZone.ZoneOccupiedEvent.Subscribe(OnEnter) }
2. Handler signatures must match the event payload exactly
trigger_device.TriggeredEvent→ payload is?agent→ handler isMyHandler(A : ?agent) : voidcapture_area_device.ZoneOccupiedEvent→ payload isagent→ handler isMyHandler(A : agent) : void
Mixing these up is a compile error. Check the device's API surface for the exact payload type.
3. Unwrap ?agent before using it
Many events send ?agent (an optional). You cannot call methods on an optional directly:
# ❌ WRONG
OnTriggered(A : ?agent) : void =
ScoreBoard.Activate(A) # compile error — A is ?agent, not agent
# ✅ CORRECT
OnTriggered(A : ?agent) : void =
if (RealAgent := A?):
ScoreBoard.Activate(RealAgent)
4. Subscribing inside a transaction that fails rolls back the subscription
Verse's transactional system means that if a Subscribe call is inside a block that fails (e.g. a failing if branch with side effects), the subscription never takes effect. Keep your Subscribe calls in straightforward, non-failing code paths.
5. Cancel() is idempotent but Subscribe is not
Calling Cancel() more than once on the same cancelable is safe — nothing bad happens. But calling Subscribe twice without cancelling the first creates two active subscriptions, so your handler fires twice per event. Always cancel before re-subscribing, or guard with an if (ZoneSubscription = false) check as shown in the walkthrough.
6. Devices must be @editable fields — you cannot instantiate them inline
# ❌ WRONG — trigger_device{} is a default struct, not a placed device
MyTrigger := trigger_device{}
MyTrigger.TriggeredEvent.Subscribe(OnFire)
# ✅ CORRECT — declared as @editable, wired in the UEFN editor
@editable MyTrigger : trigger_device = trigger_device{}