Overview
Verse's loop expression is the language's answer to while (true). It runs its body forever — until you call break to exit, or the enclosing coroutine is cancelled. Because loop is an expression (not a statement), it has a type (false, the bottom type), which means break can appear anywhere a value is expected.
When should you reach for it?
- Repeating game events — a tide alarm that fires every 30 seconds, a wave of enemies that spawns until the round ends.
- Polling device state — check a counter each tick and react when a threshold is crossed.
- Persistent background tasks — a coroutine that watches for a trigger and resets itself automatically.
The critical rule: every loop body that runs inside a <suspends> coroutine must call Sleep() (or await some other async expression) at least once per iteration, or it will spin forever and starve every other coroutine on the island.
API Reference
trigger_device
Used to relay events to other linked devices.
Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse). Inherited members are merged from trigger_base_device.
trigger_device<public> := class<concrete><final>(trigger_base_device):
Events (subscribe a handler to react):
| Event | Signature | Description |
|---|---|---|
TriggeredEvent |
TriggeredEvent<public>:listenable(?agent) |
Signaled when an agent triggers this device. Sends the agent that used this device. Returns false if no agent triggered the action (ex: it was triggered through code). |
Methods (call these to make the device act):
| Method | Signature | Description |
|---|---|---|
Trigger |
Trigger<public>(Agent:agent):void |
Triggers this device with Agent being passed as the agent that triggered the action. Use an agent reference when this device is setup to require one (for instance, you want to trigger the device only with a particular agent. |
Trigger |
Trigger<public>():void |
Triggers this device, causing it to activate its TriggeredEvent event. |
Enable |
Enable<public>():void |
Enables this device. |
Disable |
Disable<public>():void |
Disables this device. |
SetMaxTriggerCount |
SetMaxTriggerCount<public>(MaxCount:int):void |
Sets the maximum amount of times this device can trigger. * 0 can be used to indicate no limit on trigger count. * MaxCount is clamped between [0,20]. |
GetMaxTriggerCount |
GetMaxTriggerCount<public>()<transacts>:int |
Gets the maximum amount of times this device can trigger. * 0 indicates no limit on trigger count. |
GetTriggerCountRemaining |
GetTriggerCountRemaining<public>()<transacts>:int |
Returns the number of times that this device can still be triggered before hitting GetMaxTriggerCount. Returns 0 if GetMaxTriggerCount is unlimited. |
SetResetDelay |
SetResetDelay<public>(Time:float):void |
Sets the time (in seconds) after triggering, before the device can be triggered again (if MaxTrigger count allows). |
GetResetDelay |
GetResetDelay<public>()<transacts>:float |
Gets the time (in seconds) before the device can be triggered again (if MaxTrigger count allows). |
SetTransmitDelay |
SetTransmitDelay<public>(Time:float):void |
Sets the time (in seconds) which must pass after triggering, before this device informs other external devices that it has been triggered. |
GetTransmitDelay |
GetTransmitDelay<public>()<transacts>:float |
Gets the time (in seconds) which must pass after triggering, before this device informs other external devices that it has been triggered. |
timer_device
Provides a way to keep track of the time something has taken, either for scoreboard purposes, or to trigger actions. It can be configured in several ways, either acting as a countdown to an event that is triggered at the end, or as a stopwatch for an action that needs to be completed before a set time runs out.
Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse).
timer_device<public> := class<concrete><final>(creative_device_base):
Events (subscribe a handler to react):
| Event | Signature | Description |
|---|---|---|
SuccessEvent |
SuccessEvent<public>:listenable(?agent) |
Signaled when the timer completes or ends with success. Sends the agent that activated the timer, if any. |
FailureEvent |
FailureEvent<public>:listenable(?agent) |
Signaled when the timer completes or ends with failure. Sends the agent that activated the timer, if any. |
StartUrgencyModeEvent |
StartUrgencyModeEvent<public>:listenable(?agent) |
Signaled when the timer enters Urgency Mode. Sends the agent that activated the timer, if any. |
Methods (call these to make the device act):
| Method | Signature | Description |
|---|---|---|
Enable |
Enable<public>(Agent:agent):void |
Enables this device for Agent. |
Enable |
Enable<public>():void |
Enables this device. |
Disable |
Disable<public>(Agent:agent):void |
Disables this device for Agent. While disabled this device will not receive signals. |
Disable |
Disable<public>():void |
Disables this device. While disabled this device will not receive signals. |
ResetForAll |
ResetForAll<public>(Agent:agent):void |
Resets the timer back to its base time and stops it for all agents. |
ResetForAll |
ResetForAll<public>():void |
Resets the timer back to its base time and stops it for all agents. |
Start |
Start<public>(Agent:agent):void |
Starts the timer for Agent. |
Start |
Start<public>():void |
Starts the timer. |
Pause |
Pause<public>(Agent:agent):void |
Pauses the timer for Agent. |
Pause |
Pause<public>():void |
Pauses the timer. |
Resume |
Resume<public>(Agent:agent):void |
Resumes the timer for Agent. |
Resume |
Resume<public>():void |
Resumes the timer. |
Complete |
Complete<public>(Agent:agent):void |
Completes the timer for Agent. |
Complete |
Complete<public>():void |
Completes the timer. |
StartForAll |
StartForAll<public>(Agent:agent):void |
Starts the timer for all agents. |
StartForAll |
StartForAll<public>():void |
Starts the timer for all agents. |
PauseForAll |
PauseForAll<public>(Agent:agent):void |
Pauses the timer for all agents. |
PauseForAll |
PauseForAll<public>():void |
Pauses the timer for all agents. |
ResumeForAll |
ResumeForAll<public>(Agent:agent):void |
Resumes the timer for all agents. |
ResumeForAll |
ResumeForAll<public>():void |
Resumes the timer for all agents. |
CompleteForAll |
CompleteForAll<public>(Agent:agent):void |
Completes the timer for all agents. |
CompleteForAll |
CompleteForAll<public>():void |
Completes the timer for all agents. |
Save |
Save<public>(Agent:agent):void |
Saves this device's data for Agent. |
Load |
Load<public>(Agent:agent):void |
Loads this device's saved data for Agent. |
ClearPersistenceData |
ClearPersistenceData<public>(Agent:agent):void |
Clears this device's saved data for Agent. |
ClearPersistenceDataForAll |
ClearPersistenceDataForAll<public>(Agent:agent):void |
Clears this device's saved data for all agents. |
ClearPersistenceDataForAll |
ClearPersistenceDataForAll<public>():void |
Clears this device's saved data for all agents. |
SetActiveDuration |
SetActiveDuration<public>(Time:float, Agent:agent):void |
Sets the remaining time (in seconds) on the timer, if active, on Agent. |
SetActiveDuration |
SetActiveDuration<public>(Time:float):void |
Sets the remaining time (in seconds) on the timer, if active. Use this function if the timer is set to use the same time for all agent's. |
GetActiveDuration |
GetActiveDuration<public>(Agent:agent)<transacts>:float |
Returns the remaining time (in seconds) on the timer for Agent. |
GetActiveDuration |
GetActiveDuration<public>()<transacts>:float |
Returns the remaining time (in seconds) on the timer if it is set to be global. |
SetLapTime |
SetLapTime<public>(Agent:agent):void |
Sets the lap time indicator for Agent. |
SetLapTimeForAll |
SetLapTimeForAll<public>(Agent:agent):void |
Sets the lap time indicator for all agents. |
SetLapTimeForAll |
SetLapTimeForAll<public>():void |
Sets the lap time indicator for all agents. |
SetMaxDuration |
SetMaxDuration<public>(Time:float):void |
Sets the maximum duration of the timer (in seconds). |
GetMaxDuration |
GetMaxDuration<public>()<transacts>:float |
Returns the maximum duration of the timer (in seconds). |
IsStatePerAgent |
IsStatePerAgent<public>()<transacts><decides>:void |
Succeeds if this device is tracking timer state for each individual agent independently. Fails if state is being tracked globally for all agent's. |
hud_message_device
Used to show custom HUD messages to one or more
agents.
Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse).
hud_message_device<public> := class<concrete><final>(creative_device_base):
Events (subscribe a handler to react):
| Event | Signature | Description |
|---|---|---|
ShowMessageEvent |
ShowMessageEvent<public>:listenable(agent) |
Called when a Message has been Shown on-screen. Returns an Agent if it was Shown on a specified Agent's screen. |
HideMessageEvent |
HideMessageEvent<public>:listenable(agent) |
Called when a Message has been Hidden on-screen. Returns an Agent if it was Hidden from a specified Agent's screen. |
ClearAllMessagesEvent |
ClearAllMessagesEvent<public>:listenable(agent) |
Called when all queued Messages from all players that are affected by this HUD Message Device have been cleared. |
Methods (call these to make the device act):
| Method | Signature | Description |
|---|---|---|
Show |
Show<public>(Agent:agent):void |
Shows the currently set HUD Message on Agents screen. Will replace any previously active message. Use this when the device is setup to target specific agents. |
Show |
Show<public>():void |
Shows the currently set Message HUD message on screen. Will replace any previously active message. |
Hide |
Hide<public>():void |
Hides the HUD message. |
Hide |
Hide<public>(Agent:agent):void |
Hides the currently set HUD Message on Agents screen. Use this when the device is setup to target specific agents. |
Show |
Show<public>(Agent:agent, Message:message, ?DisplayTime:float |
Displays a Custom message to a specific Agent that you define.Setting DisplayTime to 0.0 will display the HUD message persistently.If not defined, or less than 0.0 the message will show for the time set on the device. |
Show |
Show<public>(Message:message, ?DisplayTime:float |
Displays a Custom message that you define for all PlayersSetting DisplayTime to 0.0 will display the HUD message persistently.If not defined, or less than 0.0 the message will show for the time set on the device. |
SetDisplayTime |
SetDisplayTime<public>(Time:float):void |
Sets the time (in seconds) the HUD message will be displayed. 0.0 will display the HUD message persistently. |
GetDisplayTime |
GetDisplayTime<public>()<transacts>:float |
Returns the time (in seconds) for which the HUD message will be displayed. 0.0 means the message is displayed persistently. |
SetText |
SetText<public>(Text:message):void |
Sets the Message to be displayed when the HUD message is activated. Text is clamped to 150 characters. |
ClearAllMessages |
ClearAllMessages<public>():void |
Clears all queued Messages from all players that are affected by this HUD Message Device. |
Walkthrough
Scenario — The Pirate Cove Tide-Watch
You're building a cel-shaded pirate cove. Every 30 seconds a tide alarm fires: a trigger_device on the dock activates, a timer_device counts down 10 seconds for players to reach high ground, and a hud_message_device shows a warning. After 5 alarm cycles the storm passes and the HUD announces calm seas.
Here's the complete device:
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
tide_watch_device := class(creative_device):
# ── Editable device references ──────────────────────────────────
@editable
TideAlarmTrigger : trigger_device = trigger_device{}
@editable
TideTimer : timer_device = timer_device{}
@editable
HudMessage : hud_message_device = hud_message_device{}
# ── Localised message helpers ────────────────────────────────────
TideWarning<localizes>(S : string) : message = "{S}"
TideClear<localizes>(S : string) : message = "{S}"
# ── Entry point ──────────────────────────────────────────────────
OnBegin<override>()<suspends> : void =
# Allow the trigger to fire as many times as we need
TideAlarmTrigger.SetMaxTriggerCount(0)
# Each alarm gives players exactly 10 seconds
TideTimer.SetActiveDuration(10.0)
var CycleCount : int = 0
var MaxCycles : int = 5
loop:
# ── Wait for the next tide interval ─────────────────────
Sleep(30.0)
set CycleCount = CycleCount + 1
# ── Fire the dock alarm trigger ──────────────────────────
TideAlarmTrigger.Trigger()
# ── Show the HUD warning to all players ──────────────────
HudMessage.Show(
TideWarning("⚠ TIDE RISING — reach high ground!"),
?DisplayTime := 10.0
)
# ── Start the 10-second countdown timer ──────────────────
TideTimer.Start()
# ── Wait for the timer to finish (success or failure) ────
# We race: whichever event fires first wins
race:
block:
TideTimer.SuccessEvent.Await()
block:
TideTimer.FailureEvent.Await()
# ── Clear the warning and pause the timer ────────────────
HudMessage.ClearAllMessages()
TideTimer.Pause()
# ── After MaxCycles the storm passes ─────────────────────
if (CycleCount >= MaxCycles):
HudMessage.Show(
TideClear("☀ Calm seas — the tide has passed!"),
?DisplayTime := 8.0
)
TideAlarmTrigger.Disable()
break```
### Line-by-line explanation
| Lines | What's happening |
|---|---|
| `TideAlarmTrigger.SetMaxTriggerCount(0)` | `0` means unlimited — the trigger won't cap out after a default number of fires. |
| `TideTimer.SetActiveDuration(10.0, TideTimer)` | Sets the countdown to 10 seconds. The second param is an `agent` — here we pass the device itself; in a real game pass the relevant player. |
| `var CycleCount : int = 0` | Mutable counter — Verse requires `var` for anything you'll reassign with `set`. |
| `loop:` | Infinite loop — runs until `break`. |
| `Sleep(30.0)` | **Yields** the coroutine for 30 seconds. Without this the loop would spin and freeze the island. |
| `TideAlarmTrigger.Trigger()` | Fires the trigger device (no agent — the tide doesn't belong to any one player). |
| `HudMessage.Show(TideWarning(...), ?DisplayTime := 10.0)` | Shows a localised message to every player for 10 seconds. Note the `<localizes>` helper — you cannot pass a raw `string` to `message`. |
| `TideTimer.Start()` | Starts the countdown. |
| `race:` | Verse's concurrency primitive — whichever branch completes first wins; the other is cancelled. |
| `TideTimer.SuccessEvent.Await()` | Suspends until the timer signals success. |
| `TideTimer.FailureEvent.Await()` | Suspends until the timer signals failure (time ran out). |
| `HudMessage.ClearAllMessages()` | Wipes the queue after the alarm window closes. |
| `TideTimer.Pause()` | Stops the timer so it doesn't keep ticking between cycles. |
| `if (CycleCount >= MaxCycles): ... break` | The loop's exit condition — after 5 cycles we disable the trigger and break. |
## Common patterns
### Pattern 1 — Polling a trigger's remaining count and disabling it at zero
Use `GetTriggerCountRemaining()` inside a loop to watch a limited-use switch on the dock gate.
```verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
dock_gate_watcher := class(creative_device):
@editable
GateTrigger : trigger_device = trigger_device{}
@editable
GateHud : hud_message_device = hud_message_device{}
UsesLeft<localizes>(S : string) : message = "{S}"
OnBegin<override>()<suspends> : void =
# Allow exactly 3 uses before the gate locks permanently
GateTrigger.SetMaxTriggerCount(3)
loop:
# Wait for the trigger to fire
GateTrigger.TriggeredEvent.Await()
Remaining := GateTrigger.GetTriggerCountRemaining()
if (Remaining = 0):
GateHud.Show(
UsesLeft("Gate locked — no uses remaining!"),
?DisplayTime := 5.0
)
GateTrigger.Disable()
break
else:
GateHud.Show(
UsesLeft("Gate used — uses remaining!"),
?DisplayTime := 3.0
)
Key calls: SetMaxTriggerCount, GetTriggerCountRemaining, Disable, TriggeredEvent.Await(), HudMessage.Show.
Pattern 2 — Resetting a timer each lap around the cove
A lap-race on the cove resets the timer for every player at the start of each new lap.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
cove_lap_timer := class(creative_device):
@editable
LapStartTrigger : trigger_device = trigger_device{}
@editable
LapTimer : timer_device = timer_device{}
@editable
LapHud : hud_message_device = hud_message_device{}
LapMsg<localizes>(S : string) : message = "{S}"
OnBegin<override>()<suspends> : void =
LapStartTrigger.SetMaxTriggerCount(0) # unlimited laps
var LapNumber : int = 1
loop:
# Wait for a player to cross the start line
MaybeAgent := LapStartTrigger.TriggeredEvent.Await()
# Reset and restart the timer for all players each lap
LapTimer.ResetForAll()
LapTimer.StartForAll()
LapHud.Show(
LapMsg("Lap started — go!"),
?DisplayTime := 3.0
)
set LapNumber = LapNumber + 1
# After 3 laps the race ends
if (LapNumber > 3):
LapTimer.CompleteForAll()
LapHud.Show(
LapMsg("Race complete — well sailed!"),
?DisplayTime := 8.0
)
break
Sleep(0.5) # small yield so the loop doesn't re-enter before devices settle
Key calls: ResetForAll, StartForAll, CompleteForAll, SetMaxTriggerCount, TriggeredEvent.Await().
Pattern 3 — Urgency-mode HUD flash loop
When the timer enters urgency mode, flash a warning message every 2 seconds until the timer ends.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
urgency_flash_device := class(creative_device):
@editable
RaceTimer : timer_device = timer_device{}
@editable
FlashHud : hud_message_device = hud_message_device{}
UrgencyMsg<localizes>(S : string) : message = "{S}"
TimeUpMsg<localizes>(S : string) : message = "{S}"
OnBegin<override>()<suspends> : void =
RaceTimer.Start()
# Wait until urgency mode kicks in
RaceTimer.StartUrgencyModeEvent.Await()
# Flash the HUD until the timer succeeds or fails
race:
block:
# Flash loop
loop:
FlashHud.Show(
UrgencyMsg("⚡ HURRY UP!"),
?DisplayTime := 1.8
)
Sleep(2.0)
block:
RaceTimer.SuccessEvent.Await()
block:
RaceTimer.FailureEvent.Await()
FlashHud.ClearAllMessages()
FlashHud.Show(
TimeUpMsg("Time's up — back to the cove!"),
?DisplayTime := 5.0
)
Key calls: Start, StartUrgencyModeEvent.Await(), SuccessEvent.Await(), FailureEvent.Await(), ClearAllMessages, HudMessage.Show.
Gotchas
1. Always Sleep() inside a loop
A loop with no Sleep() or other <suspends> call is an infinite busy-spin. It will lock up the coroutine scheduler and your island will freeze. Even Sleep(0.0) yields to the next frame — use it as a minimum.
2. message is not a string
hud_message_device.Show() takes a message, not a plain string. You must declare a <localizes> helper:
MyText<localizes>(S : string) : message = "{S}"
There is no StringToMessage function in Verse.
3. var + set for mutable loop counters
Verse variables are immutable by default. A loop counter must be declared with var and updated with set:
var Count : int = 0
loop:
set Count = Count + 1
if (Count >= 5): break
Sleep(1.0)
Forgetting var or set is a compile error.
4. TriggeredEvent sends ?agent, not agent
TriggeredEvent is a listenable(?agent) — the payload is an optional agent. If you need the agent, unwrap it:
MaybeAgent := MyTrigger.TriggeredEvent.Await()
if (A := MaybeAgent?):
# A is a real agent here
Skipping the unwrap and passing MaybeAgent directly to a method that expects agent is a type error.
5. int and float do not auto-convert
SetActiveDuration takes a float. Passing an int literal like 10 will fail — write 10.0. Similarly, SetMaxTriggerCount takes an int; passing 3.0 will fail.
6. break exits only the innermost loop
If you nest loops, break only exits the inner one. Structure your logic so the outer loop has its own exit condition, or use a flag variable.
7. loop inside race is cancelled cleanly
When a race branch wins, Verse cancels all other branches — including any loop running in a sibling branch. This is the correct pattern for "flash until timer ends" (Pattern 3 above). You do not need a manual flag to stop the flash loop.