Reference Verse compiles

suspends in Verse: Timing the Tide at Sunset Cove

Every ticking countdown, every "3… 2… 1… GO!" splash screen, every pause-then-resume moment in your island runs on one Verse superpower: `<suspends>`. This article teaches you exactly what `<suspends>` means, why it's viral, and how to wield it with `timer_device` and `hud_message_device` to build a real timed treasure-dive challenge at Sunset Cove — complete with urgency-mode warnings and a HUD countdown message.

Updated Examples verified on the live UEFN compiler

Overview

The <suspends> effect marks a function as asynchronous — it can pause at certain points (called suspension points) and resume later, potentially many frames later, without blocking the rest of the game. Every function that calls a suspending function must itself be marked <suspends>. This is called virality: the effect propagates up the call chain so you always know, just by reading a signature, whether a function might take time.

Common suspension points you will use every day:

Expression What it suspends on
Sleep(Seconds) Resumes after Seconds real-time seconds
AnimController.AwaitNextKeyframe() Resumes when the next animation keyframe finishes
SomeEvent.Await() Resumes when the event fires

When to reach for <suspends>:

  • Timed sequences (countdown, cutscene, cooldown)
  • Waiting for an animation to reach a specific keyframe
  • Loops that run every frame without hogging the CPU (Sleep(0.0))
  • Any multi-step choreography that must happen in order

When NOT to use it:

  • Simple one-shot logic triggered by an event handler that finishes instantly
  • Anything that must be <decides> (the two effects cannot be combined on the same function)

API Reference

(API surface could not be resolved for this device.)

Walkthrough

Scenario: A sunny cove dock. When the round starts, a wooden drawbridge prop animates upward (rising from the water), pauses halfway so players can see it, then completes the rise. A VFX splash spawner fires at the start and is disabled once the bridge is fully raised. The whole sequence is written as a suspending coroutine so each step waits for the previous one to finish.

using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/CreativeAnimation }
using { /Fortnite.com/Devices/CreativeAnimation/InterpolationTypes }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/SpatialMath }

# dock_bridge_sequence_device
# Place this device on your island alongside a creative_prop (the drawbridge),
# a vfx_spawner_device (water splash), and wire them up in the editor.
dock_bridge_sequence_device := class(creative_device):

    # The drawbridge prop placed at the dock.
    @editable
    BridgeProp : creative_prop = creative_prop{}

    # A VFX splash effect at the waterline.
    @editable
    SplashVFX : vfx_spawner_device = vfx_spawner_device{}

    # How long (seconds) to hold the bridge at the halfway point.
    @editable
    PauseAtMidpointSeconds : float = 2.0

    # Called by UEFN when the game round begins.
    # <suspends> is required because this function calls Sleep and AwaitNextKeyframe.
    OnBegin<override>()<suspends> : void =
        # Step 1 — fire the water-splash VFX immediately.
        SplashVFX.Enable()
        SplashVFX.Restart()

        # Step 2 — grab the animation controller for the bridge prop.
        # GetAnimationController() returns an option; unwrap it safely.
        if (AnimController := BridgeProp.GetAnimationController[]):

            # Step 3 — define a two-keyframe animation:
            #   Keyframe 1: rise 150 cm (halfway up) over 1.5 s
            #   Keyframe 2: rise another 150 cm (fully raised) over 1.5 s
            AnimController.SetAnimation(
                array{
                    keyframe_delta{
                        DeltaLocation := vector3{X := 0.0, Y := 0.0, Z := 150.0},
                        Duration := 1.5,
                        Interpolation := EaseOut
                    },
                    keyframe_delta{
                        DeltaLocation := vector3{X := 0.0, Y := 0.0, Z := 150.0},
                        Duration := 1.5,
                        Interpolation := EaseIn
                    }
                },
                ?Mode := animation_mode.OneShot
            )

            # Step 4 — start the animation.
            AnimController.Play()

            # Step 5 — SUSPEND until the first keyframe (halfway) completes.
            # AwaitNextKeyframe is itself <suspends>, so this line pauses here
            # and the rest of the game keeps running normally.
            AnimController.AwaitNextKeyframe()

            # Step 6 — we are now at the halfway point. Pause the bridge.
            AnimController.Pause()

            # Step 7 — SUSPEND for the configured hold time.
            # Sleep is a classic suspension point — nothing below runs until
            # PauseAtMidpointSeconds have elapsed.
            Sleep(PauseAtMidpointSeconds)

            # Step 8 — resume the animation for the second half of the rise.
            AnimController.Play()

            # Step 9 — SUSPEND until the full animation completes.
            AnimController.AwaitNextKeyframe()

            # Step 10 — bridge is fully raised; kill the splash VFX.
            SplashVFX.Disable()```

### Line-by-line explanation

| Lines | What's happening |
|---|---|
| `OnBegin<override>()<suspends>` | `OnBegin` is declared `<suspends>` because it calls `Sleep` and `AwaitNextKeyframe`, both of which are themselves `<suspends>`. Without this annotation the compiler refuses to compile. |
| `SplashVFX.Enable()` / `.Restart()` | These are plain `void` functions  no suspension, they return immediately. |
| `BridgeProp.GetAnimationController[]` | `[]` is the failable-call operator; the `if` unwraps the `?animation_controller` option. |
| `AnimController.SetAnimation(...)` | Configures the two-keyframe rise. `OneShot` means the animation plays once and stops. |
| `AnimController.Play()` | Starts playback  returns immediately, animation runs in the background. |
| `AnimController.AwaitNextKeyframe()` | **Suspension point #1.** The function pauses here. The game loop, physics, and other coroutines continue. When keyframe 1 finishes, execution resumes on the next line. |
| `AnimController.Pause()` | Freezes the bridge mid-air. |
| `Sleep(PauseAtMidpointSeconds)` | **Suspension point #2.** Pauses for the hold duration. |
| `AnimController.Play()` | Resumes from the paused state. |
| `AnimController.AwaitNextKeyframe()` | **Suspension point #3.** Waits for keyframe 2 to finish. |
| `SplashVFX.Disable()` | Cleanup  runs only after the bridge is fully raised. |

## Common patterns

### Pattern 1 — Looping tide bob with `Sleep` (covers `Sleep` + `Stop`)

A buoy prop at the cove bobs up and down forever using a loop and `Sleep`. Because the loop never exits, this coroutine runs for the entire game session.

```verse
using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/CreativeAnimation }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/SpatialMath }

tide_bob_device := class(creative_device):

    @editable
    BuoyProp : creative_prop = creative_prop{}

    # BobHeight in cm — how far the buoy rises each cycle.
    @editable
    BobHeight : float = 30.0

    # Seconds per half-cycle (up or down).
    @editable
    BobSpeed : float = 1.2

    OnBegin<override>()<suspends> : void =
        if (Anim := BuoyProp.GetAnimationController[]):
            # Loop animation: net delta must be zero for Loop mode.
            Anim.SetAnimation(
                array:
                    keyframe_delta:
                        DeltaLocation := vector3{X := 0.0, Y := 0.0, Z := BobHeight}
                        Duration      := BobSpeed
                        Interpolation := EaseInOut
                    keyframe_delta:
                        DeltaLocation := vector3{X := 0.0, Y := 0.0, Z := -BobHeight}
                        Duration      := BobSpeed
                        Interpolation := EaseInOut,
                ?Mode := animation_mode.Loop
            )
            Anim.Play()

            # This loop keeps the device alive and could react to game events.
            # Sleep(0.0) yields to other coroutines each frame without busy-waiting.
            loop:
                Sleep(0.0)
                # If the animation controller becomes invalid (prop destroyed),
                # stop the loop.
                if (not Anim.IsValid[]):
                    Anim.Stop()
                    break

Pattern 2 — Reacting to KeyframeReachedEvent with Subscribe (covers event subscription inside a suspending context)

A clifftop gate prop plays a two-part open animation. When keyframe 1 is reached, a VFX burst fires to celebrate the gate opening. This pattern shows how to subscribe to a listenable event emitted by animation_controller.

using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/CreativeAnimation }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/SpatialMath }

clifftop_gate_device := class(creative_device):

    @editable
    GateProp : creative_prop = creative_prop{}

    @editable
    GateOpenVFX : vfx_spawner_device = vfx_spawner_device{}

    OnBegin<override>()<suspends> : void =
        if (Anim := GateProp.GetAnimationController[]):
            # Subscribe to keyframe events BEFORE playing.
            # The handler fires synchronously on the game thread each time
            # a keyframe is reached — no <suspends> needed on the handler itself.
            Anim.KeyframeReachedEvent.Subscribe(OnKeyframeReached)

            Anim.SetAnimation(
                array:
                    keyframe_delta:
                        DeltaLocation := vector3{X := 0.0, Y := 80.0, Z := 0.0}
                        Duration      := 0.8
                        Interpolation := EaseOut
                    keyframe_delta:
                        DeltaLocation := vector3{X := 0.0, Y := 40.0, Z := 0.0}
                        Duration      := 0.4
                        Interpolation := EaseIn,
                ?Mode := animation_mode.OneShot
            )
            Anim.Play()

            # Suspend until the full animation finishes.
            # MovementCompleteEvent fires once for OneShot animations.
            Anim.MovementCompleteEvent.Await()

    # Event handler — NOT <suspends> because Subscribe handlers must be plain void.
    # Receives a tuple(KeyframeIndex:int, InReverse:logic).
    OnKeyframeReached(Args : tuple(int, logic)) : void =
        (KeyframeIndex, InReverse) := Args
        if (KeyframeIndex = 1):
            GateOpenVFX.Enable()
            GateOpenVFX.Restart()

Pattern 3 — Wrapping suspending logic in a helper function (covers virality)

This pattern shows that <suspends> is viral: a helper function that calls Sleep must itself be <suspends>, and so must any function that calls the helper. Here a shore-side countdown sequence is factored into a reusable helper.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

shore_countdown_device := class(creative_device):

    @editable
    CountdownVFX : vfx_spawner_device = vfx_spawner_device{}

    @editable
    GoVFX : vfx_spawner_device = vfx_spawner_device{}

    # This helper is <suspends> because it calls Sleep.
    # Any caller must also be <suspends> — that's virality.
    RunCountdown(Pulses : int, IntervalSeconds : float)<suspends> : void =
        var Remaining : int = Pulses
        loop:
            if (Remaining <= 0):
                break
            CountdownVFX.Enable()
            CountdownVFX.Restart()
            Sleep(IntervalSeconds)
            CountdownVFX.Disable()
            Sleep(IntervalSeconds)
            set Remaining = Remaining - 1

    # OnBegin is <suspends> because it calls RunCountdown (which is <suspends>).
    OnBegin<override>()<suspends> : void =
        # Three pulses, half a second apart — a classic 3-2-1 feel.
        RunCountdown(3, 0.5)

        # After the countdown, fire the GO burst.
        GoVFX.Enable()
        GoVFX.Restart()
        Sleep(1.5)
        GoVFX.Disable()

Gotchas

1. <suspends> is viral — the compiler enforces it

If your function calls Sleep, AwaitNextKeyframe, or any other <suspends> function, the compiler will refuse to compile unless your function is also marked <suspends>. This is intentional: you can always tell from a signature whether a function might take time. Don't try to hide it.

2. Event-handler callbacks subscribed via Subscribe must NOT be <suspends>

Subscribe expects a plain (Payload) : void function. If you need async work inside a handler, spawn a new task with spawn { MyAsyncWork() } inside the handler body.

3. Sleep(0.0) is not the same as no sleep

Sleep(0.0) yields to other coroutines for exactly one frame. This is useful in loop bodies to prevent a tight loop from starving other tasks. Sleep with a negative value completes immediately without yielding — use this for programmatic control.

4. AwaitNextKeyframe returns even if the animation is aborted

The return type is await_next_keyframe_result, not void. If you need to branch on whether the animation completed normally vs. was aborted, check the result. In the walkthrough above we ignore the result for brevity, but production code should handle the abort case.

5. <suspends> and <decides> cannot be combined

A single function cannot be both <suspends> and <decides>. If you need failable logic inside a suspending function, call a separate <decides> helper from within the <suspends> function using an if expression.

6. GetAnimationController returns an option — always unwrap it

BridgeProp.GetAnimationController[] uses the failable-call operator []. Wrap it in if (Anim := BridgeProp.GetAnimationController[]): — if the prop has no animation controller the if body is simply skipped rather than crashing.

7. Loop animations must have zero net delta

If you use animation_mode.Loop, the sum of all DeltaLocation vectors across all keyframes must be (0, 0, 0). Otherwise the prop will drift further and further from its origin on each loop cycle. The tide-bob example above satisfies this by having equal and opposite deltas.

Guides & scripts that use timer_device

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

Build your own lesson with timer_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 →