Listen to the Deep: Events Over Polling

In this lesson you'll learn to

  • βœ“Student can replace loop+Sleep polling with Subscribe/Await reactive patterns and explain why event-driven code scales.

Student can replace loop+Sleep polling with Subscribe/Await reactive patterns and explain why event-driven code scales.

πŸ“– Reference & full walkthrough: Listen to the Deep: Events Over Polling

πŸ” Builds on: north-jungle verse-events-custom (custom event bus module)

🧩 Your capstone piece: The reactive backbone: every capstone system reacts to events, nothing polls


The West Coves taught you async; the North Jungle taught you to build your own events. Down here in the Deeps, we make it law: nothing polls. A polling loop is a diver who surfaces every three seconds to ask "anything happen yet?" β€” burning air, burning frames, and always a beat behind. An event-driven system is a diver with a tether line: it hangs motionless in the dark at zero cost, and the instant something tugs, it moves. In this lesson you refactor a polling loop into Subscribe handlers and Await calls, wire them through the same event-bus module pattern you built in the North Jungle, and lay the reactive backbone every Athenaeum Depths system will hang from.

What you will build

The reactive backbone of the Athenaeum Depths capstone: a deep_events bus module (the direct descendant of your North Jungle CheckpointPassedEvent bus) plus a backbone device where:

  • Pressure-plate trigger_devices produce onto the bus (Signal),
  • a relic ledger consumes from the bus (Subscribe),
  • and the vault door suspends on the bus (Await) β€” no task ever asks "are we there yet?"

Every later Deeps system β€” the dive round, the relic prefabs, the kraken Warden β€” plugs into this same bus. That is the capstone contract: every system reacts to events; nothing polls.

Walkthrough

Step 1 β€” Meet the enemy: the polling loop

Here is the vault written the way most islands start out. It works. It is also the exact shape you are about to delete:

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

# ANTI-PATTERN β€” shown so you can refactor it. Do not ship this.
polling_vault_device := class(creative_device):

    @editable
    RelicPlate : trigger_device = trigger_device{}

    @editable
    VaultDoorTrigger : trigger_device = trigger_device{}

    @editable
    RelicsNeeded : int = 2

    var RelicsLooted : int = 0

    OnBegin<override>()<suspends> : void =
        RelicPlate.TriggeredEvent.Subscribe(OnPlateStepped)
        # THE POLL: ask the same question ten times a second, forever.
        loop:
            Sleep(0.1)
            if (RelicsLooted >= RelicsNeeded):
                break
        VaultDoorTrigger.Trigger()

    OnPlateStepped(MaybeDiver : ?agent) : void =
        set RelicsLooted += 1

Count the cost. At Sleep(0.1) this task wakes ten times every second β€” 36,000 times an hour β€” and on almost every wake the answer is "no, nothing changed." Tighten the interval and you burn more frames; loosen it and your vault opens up to a full interval late. Now multiply by every system in a capstone (air supply, ledger, wardens, doors, HUD) and you have a school of divers all surfacing on their own timers. Polling does not scale. Reacting does.

Step 2 β€” Bring in the North Jungle import: the event bus module

In the North Jungle (verse-events-custom) you decoupled checkpoints from the race manager with a CheckpointPassedEvent bus: one shared event() that producers Signal and consumers Subscribe to, so neither side knows the other exists. We reuse that exact module pattern here β€” same shape, renamed for the depths.

Step 3 β€” The reactive backbone

One complete device. Producers push, consumers react, and one task suspends at literally zero frame cost until the moment that matters:

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

reactive_backbone_device := class(creative_device):

    # The Deeps event bus β€” the SAME idea as your North Jungle
    # CheckpointPassedEvent bus, renamed for the depths. One shared box of
    # events; producers Signal into it, consumers Await out of it.
    # Fired every time a Diver loots a relic. Payload: who looted it.
    RelicLootedEvent : event(agent) = event(agent){}
    # Fired once, when the relic threshold is reached.
    VaultOpenedEvent : event(agent) = event(agent){}

    # Pressure plates in the flooded halls β€” assign both in the editor.
    @editable
    RelicPlateA : trigger_device = trigger_device{}

    @editable
    RelicPlateB : trigger_device = trigger_device{}

    # Wired to the vault door (or a sequencer) in the editor.
    @editable
    VaultDoorTrigger : trigger_device = trigger_device{}

    @editable
    RelicsNeeded : int = 2

    var RelicsLooted : int = 0

    OnBegin<override>()<suspends> : void =
        # PRODUCERS: device events feed the bus. No loop, no Sleep.
        RelicPlateA.TriggeredEvent.Subscribe(OnPlateStepped)
        RelicPlateB.TriggeredEvent.Subscribe(OnPlateStepped)

        # CONSUMER: the ledger reacts to the bus as its own task. It never asks.
        spawn{ RelicLedgerLoop() }

        Print("The Deeps are listening. Nothing polls down here.")

        # AWAIT: this task now suspends β€” costing NOTHING per frame β€”
        # until VaultOpenedEvent fires. Compare that to waking 10x/sec.
        VaultOpenedEvent.Await()
        Print("The vault groans open. The Athenaeum stirs.")
        VaultDoorTrigger.Trigger()

    # trigger_device hands us ?agent β€” unwrap it before signaling the bus,
    # so every bus consumer downstream gets a guaranteed agent.
    OnPlateStepped(MaybeDiver : ?agent) : void =
        if (Diver := MaybeDiver?):
            RelicLootedEvent.Signal(Diver)

    # Bus consumer: a custom event(t) exposes Signal and Await (not
    # Subscribe), so the ledger runs as a spawned task, suspended for
    # free between relics.
    RelicLedgerLoop()<suspends> : void =
        loop:
            Diver := RelicLootedEvent.Await()
            set RelicsLooted += 1
            Print("Relic looted ({RelicsLooted}/{RelicsNeeded})")
            if (RelicsLooted = RelicsNeeded):
                VaultOpenedEvent.Signal(Diver)

What changed, line by line:

Polling version Reactive version
loop: Sleep(0.1) ... wakes 10x/sec to check a number deep_events.VaultOpenedEvent.Await() suspends at zero cost until signaled
The loop checks RelicsLooted >= RelicsNeeded after the fact OnRelicLooted checks RelicsLooted = RelicsNeeded at the moment it changes β€” the only instant the answer can flip
Vault opens up to 0.1s late Vault opens the same frame the last relic lands
Device and vault logic tangled in one class Plates only Signal; the ledger and vault only Subscribe/Await. Neither knows the other exists

Three verbs run this whole backbone, and you already know all three from the Jungle:

  • Signal(Payload) β€” the producer's shout: it happened, here's who.
  • Subscribe(Handler) β€” react every time, without suspending. Returns a cancelable you can Cancel() later.
  • Await() β€” suspend this task until the next firing, then continue in-line. Perfect for "do A, wait for B, then C" sequencing.

Wire it up: place two Trigger devices as pressure plates and one as the vault-door trigger, assign all three @editable slots on the backbone device, and step on the plates twice. Watch the log: two ledger lines, then the vault β€” no tick spent waiting.

Common patterns

Pattern 1 β€” Await inside race{}: the air-supply timeout

The Deeps' signature move (and step 8's dive round depends on it): an event race against the clock. Whichever branch finishes first cancels the other β€” event-driven on both sides, still zero polling.

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

air_gate_device := class(creative_device):

    @editable
    SurfacePlate : trigger_device = trigger_device{}

    @editable
    DrownTrigger : trigger_device = trigger_device{}

    @editable
    AirSeconds : float = 30.0

    OnBegin<override>()<suspends> : void =
        race:
            block:
                # Branch 1: suspend until the diver reaches the surface plate.
                SurfacePlate.TriggeredEvent.Await()
                Print("Surfaced with air to spare.")
            block:
                # Branch 2: the air supply. One Sleep for the WHOLE duration
                # is a deadline, not a poll β€” the task wakes exactly once.
                Sleep(AirSeconds)
                Print("The air ran out.")
                DrownTrigger.Trigger()

Note the distinction: Sleep(0.1) in a check-loop is polling; a single Sleep(AirSeconds) as a deadline is just an event that fires once. It's the loop + ask shape that doesn't scale, not Sleep itself.

Pattern 2 β€” Await in a loop: Subscribe's suspending twin

When you want every firing but need suspending code in the reaction, loop the Await. The task costs nothing between firings:

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

relic_ticker_device := class(creative_device):

    @editable
    RelicPlate : trigger_device = trigger_device{}

    var Looted : int = 0

    OnBegin<override>()<suspends> : void =
        loop:
            # Suspend until the NEXT step β€” zero cost in between.
            RelicPlate.TriggeredEvent.Await()
            set Looted += 1
            Print("Relics recovered: {Looted}")

Rule of thumb: Subscribe for fire-and-forget reactions, Await for sequencing inside a <suspends> task, loop+Await when you need both.

Where this goes next

This lesson is order 3 of the Deeps and the load-bearing wall of the whole zone. The deep_events bus pattern is the capstone's constitution β€” every system reacts to events, nothing polls:

  • dive-trigger-volumes (next lesson) wires real pressure plates into these same TriggeredEvent producers.
  • sunken-dive-round (step 8) runs the air-supply countdown as exactly Pattern 1's race{} β€” the dive task vs. the timeout.
  • detect-player-leave subscribes cleanup handlers to player-departure events instead of scanning the roster.
  • The athenaeum-depths CAPSTONE imports the North Jungle event-bus module wholesale: relic prefabs swim when RelicLootedEvent fires, the hidden wing reveals when the pedestal ledger signals full, and the kraken Warden's state machine transitions on bus events β€” never on a timer asking questions.

Carve it on the vault door: the Deeps do not ask. The Deeps listen.

πŸ”’

Keep going β€” free

You've read the intro. The rest of this lesson is free for members. Sign in to continue and track your progress.

Sign in free to continue this lesson
Pass the quiz above to chart this quest in your Journal.

Sources

/guides/deeps-events-over-polling
Source

Biloxi Studios original lesson

Guided course

Add this lesson to your free study plan.

🧭 The Keeper’s log

Quest complete? Chart your next heading from the 🌊 The Deeps expedition.

β›΅ Take me there