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. TheOnBegintask is suspended by the scheduler, not spinning. The instantSignal()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.SubscribeandAwaitcoexist on the same event.OnRacerFinishedcongratulates every finisher (subscriber = every occurrence), while theAwaitinOnBegincatches only the first one (awaiter = next occurrence). Choose per consumer.- The
?agentunwrap inOnFinishLineCrossedis the same optional discipline from Return an Option earlier in this zone — neverSignala 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.