Reference Verse compiles

Mermaid Lagoon: await events, never poll

A Sleep-poll loop that watches a var is a deckhand who taps you on the shoulder ten times a second to ask if the race has started. Verse's answer is events: `Subscribe` to or `Await` a device's `listenable`, or mint your own signal with `event(t)`, and your code wakes the instant something happens — no loop, no lag, no wasted frames. In Mermaid Lagoon you'll build the two custom signals that glue the Coastal Race together: `RaceStartedEvent` and `RacerFinishedEvent`.

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

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:

  1. A trigger_device (pressure plate) — subscribe to TriggeredEvent to know the instant a player steps on it.
  2. A barrier_device (vault door) — call Disable() to open it.
  3. A player_reference_device — use IsReferenced() to confirm the triggering agent is the designated player, and GetStatValue() 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.

Build your own lesson with prefer_events_over_polling

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 →