Reference Verse compiles

while-style loop in Verse: Tide-Watch on the Pirate Cove

Verse doesn't have a `while` keyword — instead it gives you `loop` + `break`, an infinite loop you exit with an explicit condition. That single construct powers everything from a disappearing platform to a tide-watch countdown on a sun-drenched pirate cove. In this article you'll learn how `loop` works, why it needs `Sleep()` to yield to other coroutines, and how to wire it to real UEFN devices so your island actually reacts.

Updated Examples verified on the live UEFN compiler

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.

Guides & scripts that use trigger_device

Step-by-step tutorials that put this object to work.

Build your own lesson with trigger_device

Generate a personalized, step-by-step lesson plan built around this object — grounded in this exact reference and our compile-verified knowledge base.

Build a lesson →