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 acancelableyou canCancel()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 sameTriggeredEventproducers.sunken-dive-round(step 8) runs the air-supply countdown as exactly Pattern 1'srace{}β the dive task vs. the timeout.detect-player-leavesubscribes cleanup handlers to player-departure events instead of scanning the roster.- The
athenaeum-depthsCAPSTONE imports the North Jungle event-bus module wholesale: relic prefabs swim whenRelicLootedEventfires, 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 lessonSources
/guides/deeps-events-over-pollingBiloxi Studios original lesson
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