Overview
Every time you write loop { Sleep(0.1); if (Value > 10) { DoThing() } } you are polling — asking "has anything changed?" on a timer. The problems are real:
- Latency: you miss changes that happen between ticks.
- CPU waste: the loop runs even when nothing has changed.
- Race conditions: a value can change and change back before your next check.
Verse devices expose listenable events — signals that fire exactly when something happens. Subscribing to them with .Subscribe() means your handler runs the moment the event fires, with zero polling overhead. This article teaches you to recognize polling anti-patterns and replace them with clean event-driven code using real UEFN devices.
API Reference
(API surface could not be resolved for this device.)
Walkthrough
Scenario: A vault door that unlocks when a player steps on a pressure plate
A classic polling approach would loop and check whether a player is standing on the plate. Instead, we subscribe to the trigger device's TriggeredEvent and the player reference device's state — reacting instantly.
This example wires up:
- A
trigger_device(pressure plate) — subscribe toTriggeredEventto know the instant a player steps on it. - A
barrier_device(vault door) — callDisable()to open it. - A
player_reference_device— useIsReferenced()to confirm the triggering agent is the designated player, andGetStatValue()to check a tracked stat before unlocking.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# vault_door_manager
# Subscribes to a pressure plate trigger and a player reference device.
# Opens the vault barrier ONLY when the referenced player steps on the plate
# AND their tracked stat (e.g. keys collected) is >= 3.
vault_door_manager := class(creative_device):
# The pressure plate in the world
@editable
PressurePlate : trigger_device = trigger_device{}
# The barrier acting as the vault door
@editable
VaultDoor : barrier_device = barrier_device{}
# Tracks how many keys the player has collected
@editable
KeyTracker : player_reference_device = player_reference_device{}
# Called once when the game starts
OnBegin<override>()<suspends> : void =
# Subscribe — no loop, no Sleep, no polling.
# OnPlateTriggered fires the instant any agent steps on the plate.
PressurePlate.TriggeredEvent.Subscribe(OnPlateTriggered)
# Keep this coroutine alive so subscriptions remain active
Sleep(Inf)
# Handler: fires immediately when the trigger fires
# TriggeredEvent sends ?agent — we must unwrap it
OnPlateTriggered(TriggeringAgent : ?agent) : void =
if (Agent := TriggeringAgent?):
# IsReferenced<decides> — check if this is our designated player
if (KeyTracker.IsReferenced[Agent]):
# GetStatValue returns the int the tracker is watching
KeyCount := KeyTracker.GetStatValue()
if (KeyCount >= 3):
# Open the vault — event-driven, instant, no loop needed
VaultDoor.Disable()
Line-by-line explanation:
| Line | What it does |
|---|---|
PressurePlate.TriggeredEvent.Subscribe(OnPlateTriggered) |
Registers our handler. The runtime calls OnPlateTriggered the instant the event fires — no polling. |
Sleep(Inf) |
Keeps OnBegin alive (required for subscriptions to stay active) without burning CPU in a check loop. |
OnPlateTriggered(TriggeringAgent : ?agent) |
TriggeredEvent sends ?agent — the handler signature must match. |
if (Agent := TriggeringAgent?) |
Unwraps the option. If no agent triggered it (e.g. a prop), we bail safely. |
KeyTracker.IsReferenced[Agent] |
IsReferenced is <decides> — call it inside if with [] syntax. |
KeyTracker.GetStatValue() |
Reads the current tracked integer (keys collected) without a loop. |
VaultDoor.Disable() |
Opens the door exactly once, exactly when conditions are met. |
Common patterns
Pattern 1 — React to a stat crossing a threshold (no polling loop)
Instead of loop { Sleep(1.0); if (Tracker.GetStatValue() >= 10) { ... } }, subscribe to a tracker's event and read the value only when it changes.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# score_threshold_manager
# Opens a reward chest the moment a player's score hits 10.
# Uses event subscription — never polls GetStatValue() in a loop.
score_threshold_manager := class(creative_device):
@editable
ScoreTracker : tracker_device = tracker_device{}
@editable
RewardChest : item_spawner_device = item_spawner_device{}
@editable
ScoreThreshold : int = 10
OnBegin<override>()<suspends> : void =
# TargetProgressChangedEvent fires whenever the tracked value changes
# — subscribe once, react every time, zero polling.
ScoreTracker.TargetProgressChangedEvent.Subscribe(OnScoreChanged)
Sleep(Inf)
OnScoreChanged(Agent : agent) : void =
# GetValue(Agent) reads the current value only when it actually changed
CurrentScore := ScoreTracker.GetValue(Agent)
if (CurrentScore >= ScoreThreshold):
RewardChest.SpawnItem()
Pattern 2 — Confirm the referenced player before acting (IsReferenced)
Use IsReferenced inside an event handler to gate logic to a specific player — no polling loop watching who is nearby.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# vip_zone_manager
# Grants a weapon only to the VIP player when they enter a zone.
# The player_reference_device identifies the VIP; IsReferenced gates the grant.
vip_zone_manager := class(creative_device):
@editable
ZoneTrigger : trigger_device = trigger_device{}
@editable
VIPReference : player_reference_device = player_reference_device{}
@editable
WeaponGranter : item_granter_device = item_granter_device{}
OnBegin<override>()<suspends> : void =
ZoneTrigger.TriggeredEvent.Subscribe(OnZoneEntered)
Sleep(Inf)
OnZoneEntered(MaybeAgent : ?agent) : void =
if (Agent := MaybeAgent?):
# IsReferenced<decides> — only true for the designated VIP
if (VIPReference.IsReferenced[Agent]):
WeaponGranter.GrantItem(Agent)
Pattern 3 — Replace a polling health-check with a race + event
Instead of looping to check whether a player is eliminated, race two async paths: the elimination event and a timeout. The first to complete wins — clean, instant, no Sleep loop.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# elimination_race_manager
# Starts a countdown. If the player is eliminated before time runs out,
# the elimination branch wins. No polling loop checks player state.
elimination_race_manager := class(creative_device):
@editable
EliminationManager : elimination_manager_device = elimination_manager_device{}
@editable
TimeLimitSeconds : float = 30.0
@editable
BonusGranter : item_granter_device = item_granter_device{}
var SurvivorAgent : ?agent = false
OnBegin<override>()<suspends> : void =
EliminationManager.EliminationEvent.Subscribe(OnEliminated)
Sleep(Inf)
OnEliminated(Result : elimination_result) : void =
# Spawn a bonus for the eliminator — event fires instantly on kill,
# no loop was checking for it.
if (Eliminator := Result.EliminatingAgent?):
BonusGranter.GrantItem(Eliminator)
Gotchas
1. ?agent unwrapping is mandatory
TriggeredEvent and similar events send ?agent (an option type). If you write OnTriggered(A : agent) the handler signature won't match and the subscription silently does nothing — or fails to compile. Always match the exact type:
# WRONG — signature mismatch
OnTriggered(A : agent) : void = ...
# CORRECT — unwrap inside the body
OnTriggered(MaybeAgent : ?agent) : void =
if (Agent := MaybeAgent?):
# safe to use Agent here
2. IsReferenced is <decides> — use [] inside if
IsReferenced can fail (returns nothing if the agent isn't the referenced player). Call it with bracket syntax inside an if:
# WRONG
if (VIPRef.IsReferenced(Agent)):
# CORRECT
if (VIPRef.IsReferenced[Agent]):
3. GetStatValue() returns int, not float
Don't compare it to a float literal. Verse does not auto-convert:
# WRONG — type mismatch
if (KeyTracker.GetStatValue() >= 3.0):
# CORRECT
if (KeyTracker.GetStatValue() >= 3):
4. Sleep(Inf) is required to keep subscriptions alive
If OnBegin returns, all subscriptions registered inside it are torn down. End OnBegin with Sleep(Inf) to keep the device alive for the session.
5. Don't mix polling and events for the same state
If you subscribe to an event and poll the same value in a loop, you'll handle changes twice and introduce race conditions. Pick one approach — events are almost always correct.