Kraken's Cove: race — first task wins, losers cancelled
In this lesson you'll learn to
- ✓Student can use race{} to run competing tasks where the first to finish cancels the rest — the timeout pattern.
Student can use race{} to run competing tasks where the first to finish cancels the rest — the timeout pattern.
📖 Reference & full walkthrough: Kraken's Cove: race — first task wins, losers cancelled
🧩 Your capstone piece: Storm-vs-finish-line: race{AwaitAllFinished(), StormTimer()} ends the race either way (FAILS compile 1 err — rewrite first; central to zone theme)
What you'll learn
You'll learn how the race expression runs two Await() calls concurrently and lets you react to whichever trigger fires first. Just as important, we do something real with the result: build a canvas widget, add it to each player's screen with GetPlayerUI[] and AddWidget, and overwrite a text_block to announce the winner.
How it works
A race block runs two or more async expressions simultaneously. The instant one completes, race returns that expression's value and cancels every other branch — no polling, no manual cancellation. It is the idiomatic way to express "wait for A OR B".
The pieces we need:
- Two
trigger_devicereferences — each exposesTriggeredEvent, a listenable whoseAwait()is<suspends>and resumes when the trigger fires. BecauseAwait()returns an optional agent (?agent), we ignore that value and instead have each branch end in astringlabel. - A
race— we place each branch in ablock:so the last expression of the winning branch (astring) becomes theraceresult. The result type is the common type of every branch, so all branches must yield astring. - Player UI — for each player we call
GetPlayerUI[Player](fallible, so it lives inside anif), build acanvasholding atext_block, andAddWidgetit. When a trigger wins we update the text withSetText.
Two correctness details drive this code:
GetPlayerUI[Player]is fallible (note the[]), so it MUST appear inside anif (...)condition — never as a bare statement.text_blocktext fields (DefaultText,SetText) want amessage, not astringâ so the banner’s default comes from a<localizes>message constant, and the winner label goes through a tiny<localizes>helper that converts the text.
Let's build it
Place two trigger_device actors and bind the @editable fields TriggerA and TriggerB.
using { /Verse.org/Simulation }
using { /Fortnite.com/Devices }
using { /Fortnite.com/UI }
using { /UnrealEngine.com/Temporary/UI }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /UnrealEngine.com/Temporary/Diagnostics }
# DefaultText needs a message (not a string), so define a <localizes> constant.
WaitingLabel<localizes> : message = "Waiting for a trigger..."
race_trigger_demo := class<concrete>(creative_device):
@editable TriggerA : trigger_device = trigger_device{}
@editable TriggerB : trigger_device = trigger_device{}
# Shared banner whose text we overwrite with the race winner.
Banner : text_block = text_block{DefaultText := WaitingLabel}
# text_block.SetText needs a message, so wrap the string here.
StringToMessage<localizes>(Value : string) : message = "{Value}"
OnBegin<override>()<suspends>: void =
# Build one HUD banner per player using the real player-UI API.
for (Player : GetPlayspace().GetPlayers()):
# GetPlayerUI[] is fallible, so it lives inside an if condition.
if (UI := GetPlayerUI[Player]):
Root : canvas = canvas:
Slots := array:
canvas_slot:
Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.1}, Maximum := vector2{X := 0.5, Y := 0.1}}
Alignment := vector2{X := 0.5, Y := 0.0}
Offsets := margin{Top := 50.0, Left := 0.0, Right := 0.0, Bottom := 0.0}
SizeToContent := true
Widget := Banner
# AddWidget makes the canvas visible on this player's screen.
UI.AddWidget(Root)
# Keep racing so every press is announced.
loop:
# race runs both Awaits at once; the first trigger to fire wins and
# its block's string becomes the race result. The loser is canceled.
Winner : string =
race:
block:
TriggerA.TriggeredEvent.Await()
"Trigger A won the race!"
block:
TriggerB.TriggeredEvent.Await()
"Trigger B won the race!"
Print("{Winner}")
# Overwrite the on-screen banner with the winner.
Banner.SetText(StringToMessage(Winner))
Sleep(0.5) # Brief debounce before re-arming the race.
Try it yourself
- Add a third branch that races a
Sleep(10.0)timeout: if neither trigger fires within ten seconds, set the banner to"Nobody raced in time!". Remember every branch must still end in astring. - Give each player their OWN
text_blockinstead of a sharedBannerso only the player who steps on a trigger sees the result — store them in a map keyed by player. - Swap
Sleep(0.5)for a longer debounce, or remove it, and observe how quickly the race re-arms.
Recap
race runs its sibling async branches concurrently, returns the first to finish, and cancels the rest — a clean structured early-exit. We wrapped each branch in a block: so it produces a string label, captured that as the winner, and drove a real HUD by pairing GetPlayerUI[] (fallible, hence inside if), a canvas with a text_block, and SetText fed by a <localizes> string-to-message helper. That is race doing genuine work end to end.
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/sug-implementing-race-expressions-for-concurrent-verse-tasksBiloxi Studios original lesson
Add this lesson to your free study plan.
🧭 The Keeper’s log
Quest complete? Chart your next heading from the 🦑 West Coves expedition.
⛵ Take me there