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.