Overview
In Verse, devices broadcast events — signals that fire when something meaningful happens: a timer runs out, a score is awarded, a tracked agent changes. The Subscribe() method on any listenable event lets you register a callback function that Verse calls automatically every time that event fires.
This is the backbone of reactive island design. Instead of polling or busy-waiting, you declare "when X happens, do Y" and Verse handles the rest. You'll reach for Subscribe() whenever:
- A
timer_devicesucceeds or fails and you need to trigger a cinematic or award points. - A
score_manager_deviceawards points and you want to update a leaderboard or play a fanfare. - A
player_reference_deviceswaps its tracked agent and you need to re-register game state.
The three golden rules of subscribing:
- Declare the device as an
@editablefield in yourclass(creative_device)— you cannot reference a placed device any other way. - Call
Subscribe()insideOnBegin— that's when your device initializes and the island is live. - Match your handler's parameter type exactly — a
listenable(agent)event hands your handler anagent, alistenable(?agent)hands an?agent(option), andlistenable(tuple())hands nothing useful.
API Reference
(API surface could not be resolved for this device.)
Walkthrough
The Scenario: A sunny cove on Verse Island. Players have 60 seconds to reach the clifftop lighthouse. A timer_device counts down. If they succeed, a cinematic_sequence_device fires the lighthouse beacon sweep. A score_manager_device awards points to whoever triggered the timer. If time runs out, the cinematic plays in reverse — the beacon dies. A player_reference_device tracks the current leader so the score goes to the right person.
Place these devices in your UEFN level, assign them in the Details panel, and drop this Verse device alongside them.
using { /Fortnite.com }
using { /Verse.org }
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# Localised message helper — required because message params don't accept raw strings
LighthouseMsg<localizes>(S : string) : message = "{S}"
cove_lighthouse_manager := class(creative_device):
# ── Editable device references ──────────────────────────────────────
# Assign each of these in the UEFN Details panel after placing the device.
@editable
CoveTimer : timer_device = timer_device{}
@editable
BeaconCinematic : cinematic_sequence_device = cinematic_sequence_device{}
@editable
ScoreManager : score_manager_device = score_manager_device{}
# Note: player_reference_device is not available in the current API grounding.
# @editable
# LeaderReference : creative_device_base = creative_device_base{}
# ── Lifecycle ───────────────────────────────────────────────────────
OnBegin<override>()<suspends> : void =
# Wire up all three event subscriptions here in OnBegin.
# Subscribe returns a cancellable token, but for always-on listeners
# we can discard it with the _ pattern.
# 1. Timer success → fire the lighthouse beacon cinematic + award score
CoveTimer.SuccessEvent.Subscribe(OnTimerSuccess)
# 2. Timer failure → reverse the cinematic (beacon goes dark)
CoveTimer.FailureEvent.Subscribe(OnTimerFailure)
# 3. Score awarded → update the player reference so the HUD knows who leads
ScoreManager.ScoreOutputEvent.Subscribe(OnScoreAwarded)
# 4. Player reference updated → log the swap (good for debugging)
# LeaderReference.AgentUpdatedEvent.Subscribe(OnLeaderChanged)
# Start the countdown — the island is live!
# Using Enable() as Start() requires an agent parameter in the current API.
CoveTimer.Enable()
# ── Handlers ────────────────────────────────────────────────────────
# SuccessEvent is listenable(?agent) — the payload is an option agent.
# We MUST unwrap it before we can use it as an agent.
OnTimerSuccess(MaybeAgent : ?agent) : void =
# Play the beacon sweep for everyone
BeaconCinematic.Play()
# Award points only if we have a real agent
if (A := MaybeAgent?):
ScoreManager.Activate(A)
# FailureEvent is also listenable(?agent)
OnTimerFailure(MaybeAgent : ?agent) : void =
# Run the cinematic backwards — beacon fades out
BeaconCinematic.PlayReverse()
# ScoreOutputEvent is listenable(agent) — no option, direct agent
OnScoreAwarded(ScoredAgent : agent) : void =
# Register this agent as the new leader in the reference device
# LeaderReference.Register(ScoredAgent)
# AgentUpdatedEvent is listenable(agent)
# OnLeaderChanged(NewLeader : agent) : void =
# # In a real island you might update a UI element here.
# # For now we just verify the reference is correct.
# if (LeaderReference.IsReferenced[NewLeader]):
# # NewLeader is confirmed as the tracked agent — safe to act on
# ScoreManager.Activate(NewLeader)```
### Line-by-line explanation
| Lines | What's happening |
|---|---|
| `@editable` fields | Each device is declared as a field so UEFN can wire the placed instance to this script. Without `@editable` the field is a default empty device that does nothing. |
| `OnBegin` | Verse calls this once when the island starts. All four `Subscribe()` calls happen here so listeners are active for the whole session. |
| `CoveTimer.SuccessEvent.Subscribe(OnTimerSuccess)` | `SuccessEvent` is `listenable(?agent)`. Verse will call `OnTimerSuccess` with a `?agent` payload every time the timer succeeds. |
| `CoveTimer.FailureEvent.Subscribe(OnTimerFailure)` | Same pattern for the failure path — beacon goes dark. |
| `ScoreManager.ScoreOutputEvent.Subscribe(OnScoreAwarded)` | `ScoreOutputEvent` is `listenable(agent)` — no option wrapper needed. |
| `LeaderReference.AgentUpdatedEvent.Subscribe(OnLeaderChanged)` | Fires whenever the reference device's tracked agent changes. |
| `if (A := MaybeAgent?):` | Option unwrap — the `?` suffix attempts to extract the inner value. If the timer fired without a specific agent the block is skipped safely. |
| `BeaconCinematic.Play()` / `PlayReverse()` | Calling different overloads of the cinematic device based on success vs failure. |
| `LeaderReference.IsReferenced[NewLeader]` | A `<decides>` function — called inside `if` with `[]` brackets, not `()`. It fails (and the if-branch is skipped) if the agent isn't the referenced one. |
## Common patterns
### Pattern 1 — Cinematic plays in reverse when the timer fails
A standalone snippet showing how to subscribe to `FailureEvent` and call `PlayReverse()` with an agent overload when one is available.
```verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
cove_beacon_failure := class(creative_device):
@editable
RaceTimer : timer_device = timer_device{}
@editable
BeaconCinematic : cinematic_sequence_device = cinematic_sequence_device{}
OnBegin<override>()<suspends> : void =
RaceTimer.FailureEvent.Subscribe(OnFail)
RaceTimer.Start()
# FailureEvent sends ?agent — unwrap to get the agent overload of PlayReverse
OnFail(MaybeAgent : ?agent) : void =
if (A := MaybeAgent?):
# Play reverse for the specific instigating agent
BeaconCinematic.PlayReverse(A)
else:
# No agent — play reverse for everyone
BeaconCinematic.PlayReverse()
Pattern 2 — Score manager awards points and tracks max triggers
Subscribe to both ScoreOutputEvent and MaxTriggersEvent to react differently when the score cap is hit.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
cove_score_watcher := class(creative_device):
@editable
Scorer : score_manager_device = score_manager_device{}
@editable
FinalCinematic : cinematic_sequence_device = cinematic_sequence_device{}
OnBegin<override>()<suspends> : void =
# React every time any agent scores
Scorer.ScoreOutputEvent.Subscribe(OnScored)
# React when the score device hits its trigger cap
Scorer.MaxTriggersEvent.Subscribe(OnMaxTriggersReached)
OnScored(ScoringAgent : agent) : void =
# Increment the next award by 1 for a streak bonus
Scorer.Increment(ScoringAgent)
OnMaxTriggersReached(LastAgent : agent) : void =
# The score device is exhausted — play the victory cinematic
FinalCinematic.Play(LastAgent)
Pattern 3 — Player reference device tracks the leader; react to swaps
Subscribe to AgentReplacedEvent (a different agent was swapped in) vs AgentUpdatedEvent (the same slot was refreshed) to handle leaderboard changes cleanly.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
cove_leader_tracker := class(creative_device):
@editable
LeaderRef : player_reference_device = player_reference_device{}
@editable
Scorer : score_manager_device = score_manager_device{}
OnBegin<override>()<suspends> : void =
# AgentReplacedEvent fires when a DIFFERENT agent takes the lead
LeaderRef.AgentReplacedEvent.Subscribe(OnLeaderReplaced)
# AgentUpdateFailsEvent fires when a registration attempt is rejected
LeaderRef.AgentUpdateFailsEvent.Subscribe(OnLeaderUpdateFailed)
# AgentReplacedEvent is listenable(agent) — new leader arrives
OnLeaderReplaced(NewLeader : agent) : void =
# Award a bonus to the new leader for taking the top spot
Scorer.Activate(NewLeader)
# AgentUpdateFailsEvent is listenable(agent) — the candidate was rejected
OnLeaderUpdateFailed(FailedCandidate : agent) : void =
# Optionally disable scoring for this agent until they qualify
Scorer.Disable(FailedCandidate)
Gotchas
1. ?agent vs agent — always check the event's type
timer_device.SuccessEvent and FailureEvent are listenable(?agent) — the payload is an option. If you write OnSuccess(A : agent) your code won't compile. Write OnSuccess(MaybeAgent : ?agent) and unwrap with if (A := MaybeAgent?): before using A as an agent.
2. <decides> functions use [] inside if, not ()
LeaderReference.IsReferenced[NewLeader] — note the square brackets. <decides> functions can fail, so Verse requires them inside a failure context (if, or, etc.). Using () is a compile error.
3. Devices MUST be @editable fields — bare variables don't work
If you write MyTimer := timer_device{} as a local variable and call MyTimer.SuccessEvent.Subscribe(...), you're talking to a default empty device, not the one you placed in the level. Always declare placed devices as @editable class fields and assign them in the UEFN Details panel.
4. Subscribe in OnBegin, not in a spawned task
Subscriptions made inside spawn { } blocks can race against events that fire early. Wire all your listeners at the top of OnBegin before you start any timers or other devices.
5. message params need a localizer function — no raw strings
If any of your handlers display text (e.g. via a HUD device), you cannot pass a raw string where message is expected. Declare a localizer: MyMsg<localizes>(S:string):message = "{S}" and call MyMsg("Lighthouse lit!"). There is no StringToMessage function in Verse.
6. StoppedEvent on cinematic_sequence_device sends tuple(), not agent
BeaconCinematic.StoppedEvent is listenable(tuple()). Its handler signature is OnStopped(_ : tuple()) : void — you can't extract an agent from it. Use SuccessEvent/FailureEvent on the timer if you need agent context after a cinematic ends.