Reference Devices 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.

Inkbeard has eight arms, and not one of them is for tapping the table asking "has the race started yet?" every tenth of a second. That is what a Sleep-poll loop does: it burns simulation time checking a var that has not changed, and it still reacts late — up to a full poll interval after the moment actually happened. Verse gives you a better deal. Devices broadcast listenable events you can Subscribe to or Await, and the event(t) class lets you mint your OWN signals. When something happens, your code wakes instantly, exactly once, with the payload in hand. No loop. No lag. No wasted frames.

What you will build

The signal glue of the Coastal Race capstone: a race_signal_hub device that owns two custom signals — RaceStartedEvent : event() and RacerFinishedEvent : event(agent) — and wires real device events (a start button_device, a finish-line trigger_device) into them. Every phase of the race (countdown, racing, podium) will hang off these two signals rather than polling shared flags. This is a direct reuse of the event-bus pattern from Barnaby's North Jungle lesson "Make Your Own Event" (verse-events-custom) — there you declared BossDefeatedEvent : event(agent) and learned Signal/Subscribe; here the same shape becomes the spine of a multiplayer race.

Walkthrough

Step 0 — the anti-pattern you are replacing

First, look the enemy in the eye. This compiles, it even works, and it is still wrong:

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

polling_race_gate := class(creative_device):

    @editable
    StartButton : button_device = button_device{}

    # The flag being watched. The poll loop below is the anti-pattern.
    var RaceStarted : logic = false

    OnBegin<override>()<suspends> : void =
        StartButton.InteractedWithEvent.Subscribe(OnStartPressed)
        # ANTI-PATTERN: wake up 10 times a second to ask "yet? yet? yet?"
        loop:
            if (RaceStarted?):
                break
            Sleep(0.1)
        Print("Race started - after up to 100ms of dead air.")

    OnStartPressed(Presser : agent) : void =
        set RaceStarted = true

Two problems. Latency: the loop only notices RaceStarted on its next wake-up, so the reaction is up to 0.1s late — and shrinking the Sleep just trades lag for wasted work. Cost: the task wakes ten times a second forever, even when nothing is happening. Multiply that by every system that needs to know about every phase change and your island is spending its frame budget asking questions the simulation could have answered for free.

Step 1 — declare the custom signals

Import the event-bus pattern from the North Jungle. An event(t) is declared like any field, with the payload type in the parentheses:

RaceStartedEvent : event() = event(){}          # no payload - a pure "it happened"
RacerFinishedEvent : event(agent) = event(agent){}  # payload: WHO finished

event() needs using { /Verse.org/Verse }. It gives you three verbs:

Verb Who calls it What it does
Signal(Payload) the code where the thing HAPPENS wakes every awaiter, calls every subscriber
Await() a <suspends> task suspends free of charge until the next Signal, returns the payload
Subscribe(Handler) setup code (usually OnBegin) registers a function called on EVERY future Signal

Step 2 — feed device events into your signals

Devices already speak this language: button_device.InteractedWithEvent is a listenable(agent), and trigger_device.TriggeredEvent is a listenable(?agent) (the optional matters — a non-agent cause can trip a trigger). Handlers translate device moments into game-meaning signals: button press becomes RaceStartedEvent.Signal(), finish-line crossing becomes RacerFinishedEvent.Signal(Racer).

Step 3 — the complete signal hub

Here is the full device. Note how OnBegin reads like the story of the race — subscribe, await the start, await the winner — with zero loops:

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Verse }
using { /UnrealEngine.com/Temporary/Diagnostics }

# Localized HUD message helpers
RaceLiveText<localizes>(S : string) : message = "{S}"
FinishText<localizes>(S : string) : message = "{S}"

race_signal_hub := class(creative_device):

    # --- Wire these in the UEFN editor ---
    @editable
    StartButton : button_device = button_device{}

    @editable
    FinishLine : trigger_device = trigger_device{}

    @editable
    RaceHUD : hud_message_device = hud_message_device{}

    # --- Custom signals: the event-bus pattern from Barnaby's jungle lesson ---
    # Same declaration shape as BossDefeatedEvent : event(agent) from
    # north-jungle's "Make Your Own Event" — reused here as race glue.
    RaceStartedEvent : event() = event(){}
    RacerFinishedEvent : event(agent) = event(agent){}

    OnBegin<override>()<suspends> : void =
        RaceHUD.SetDisplayTime(3.0)

        # Device events FEED the custom signals...
        StartButton.InteractedWithEvent.Subscribe(OnStartPressed)
        FinishLine.TriggeredEvent.Subscribe(OnFinishLineCrossed)

        # Custom event(t) signals support Signal + Await (not Subscribe -
        # Subscribe lives on device listenables); awaiters below do the work.

        # AWAIT the start. This task suspends at ZERO cost — no loop,
        # no Sleep, no wasted frames — until someone calls Signal().
        RaceStartedEvent.Await()
        RaceHUD.Show(RaceLiveText("The race is LIVE - through the lagoon!"))

        # AWAIT the first finisher. event(agent).Await() RETURNS the payload.
        Winner := RacerFinishedEvent.Await()
        RaceHUD.Show(Winner, FinishText("First across the line - champion!"))
        Print("Race complete. A winner has been crowned.")

    OnStartPressed(Presser : agent) : void =
        # ONE Signal wakes every awaiter and notifies every subscriber.
        RaceStartedEvent.Signal()

    OnFinishLineCrossed(MaybeRacer : ?agent) : void =
        # trigger_device sends ?agent - unwrap before signalling.
        if (Racer := MaybeRacer?):
            RacerFinishedEvent.Signal(Racer)
            OnRacerFinished(Racer)

    OnRacerFinished(Racer : agent) : void =
        RaceHUD.Show(Racer, FinishText("You crossed the line!"))

What to notice:

  • RaceStartedEvent.Await() costs nothing while waiting. The OnBegin task is suspended by the scheduler, not spinning. The instant Signal() fires it resumes — same frame, no poll-interval lag.
  • event(agent).Await() returns the payload. Winner := RacerFinishedEvent.Await() hands you the finishing agent directly, exactly like awaiting a device event in the companion lesson Awaiting Events in Verse.
  • Subscribe and Await coexist on the same event. OnRacerFinished congratulates every finisher (subscriber = every occurrence), while the Await in OnBegin catches only the first one (awaiter = next occurrence). Choose per consumer.
  • The ?agent unwrap in OnFinishLineCrossed is the same optional discipline from Return an Option earlier in this zone — never Signal a payload you have not proven exists.

Step 4 — test it in the lagoon

Place a button, a trigger, and a HUD message device; wire them to the @editable slots. Press the button: the HUD announces the race instantly. Drive a boat through the trigger: the finish message fires the moment you cross — not a tenth of a second later.

Common patterns

Pattern 1 — await a device event directly

When ONE task is waiting for ONE device moment, you do not even need a custom event — await the device's listenable inline, as a sequencing step:

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Verse }
using { /UnrealEngine.com/Temporary/Diagnostics }

lagoon_finish_watcher := class(creative_device):

    @editable
    FinishLine : trigger_device = trigger_device{}

    OnBegin<override>()<suspends> : void =
        # No custom event needed when ONE task waits for ONE device moment:
        # await the device event directly, in sequence.
        MaybeRacer := FinishLine.TriggeredEvent.Await()
        if (Racer := MaybeRacer?):
            Print("A racer crossed the finish line!")
        else:
            Print("Something non-agent tripped the finish line.")

Pattern 2 — an event() as a start gate between tasks

A payload-free event() makes a clean rendezvous point: many spawned tasks park on Await(), and one Signal() releases the whole fleet — the exact mechanic behind the capstone's synchronized 3-2-1-GO:

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Verse }
using { /UnrealEngine.com/Temporary/Diagnostics }

countdown_gate := class(creative_device):

    # One event(), many awaiters: a start gate between concurrent tasks.
    GoEvent : event() = event(){}

    OnBegin<override>()<suspends> : void =
        # Two watcher tasks park themselves on the gate...
        spawn { LagoonWatcher() }
        spawn { ChannelWatcher() }

        # ...while the countdown runs. ONE Signal releases them all at once.
        Print("3...")
        Sleep(1.0)
        Print("2...")
        Sleep(1.0)
        Print("1...")
        Sleep(1.0)
        GoEvent.Signal()

    LagoonWatcher()<suspends> : void =
        GoEvent.Await()
        Print("Lagoon gate open - GO!")

    ChannelWatcher()<suspends> : void =
        GoEvent.Await()
        Print("Channel gate open - GO!")

Where this goes next

These two signals are the glue of the Coastal Race capstone (west-coves-coastal-race). The state machine from Simple Game State Machine transitions Lobby to Racing when RaceStartedEvent fires; the checkpoint sensors from Trigger Device Checkpoints feed RacerFinishedEvent; the race{}/rush{} expressions from earlier in this zone await these signals instead of watching flags. And the pattern itself travels: any time two systems on your island need to talk without holding hands, declare an event(t), Signal it where the thing happens, and let everyone else Await or Subscribe. Inkbeard keeps all eight arms free that way.

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 →