Overview
The Spawn Wave Pattern describes the technique of using a timer_device as a metronome that drives repeated cycles of spawning, scoring, and cinematics. Each time the timer fires its SuccessEvent, your Verse device advances the wave counter, awards points to surviving players, plays a cinematic sting, and re-arms the timer for the next wave.
Use this pattern when you need:
- Escalating difficulty (more spawns, less time between waves)
- Per-wave score rewards tied to survival
- Cinematic punctuation (a horn blast, a wave crash) between rounds
- A clean way to end the game after a fixed number of waves
The four devices you'll place on your cove island:
| Device | Role |
|---|---|
timer_device |
Counts down each wave interval |
player_spawner_device |
Respawns eliminated players at the dock |
score_manager_device |
Awards points for surviving each wave |
cinematic_sequence_device |
Plays a cel-shaded wave-crash sting |
API Reference
(API surface could not be resolved for this device.)
Walkthrough
Scenario: A 2D cel-shaded cove island. Players defend the dock. Every 30 seconds a new wave begins. Survivors earn 100 points. After 5 waves the cinematic plays one final time and the game ends. Eliminated players respawn at the shore spawner.
Place these devices in UEFN and wire them to the Verse device via the @editable fields:
- One
timer_device(set to 30 s countdown, Does Not Auto-Start) - One
player_spawner_deviceat the dock - One
score_manager_device(set to award 100 points per activation) - One
cinematic_sequence_device(your wave-crash sequence)
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/Patchwork }
using { /Verse.org/Simulation }
using { /Verse.org/Native }
# Localized message helper
WaveMessage<localizes>(S : string) : message = "{S}"
cove_wave_manager := class(creative_device):
# Wire these in the UEFN Details panel
@editable WaveTimer : timer_device = timer_device{}
@editable DockSpawner : player_spawner_device = player_spawner_device{}
@editable ScoreManager : score_manager_device = score_manager_device{}
@editable WaveCinematic : cinematic_sequence_device = cinematic_sequence_device{}
# Mutable wave state
var CurrentWave : int = 0
MaxWaves : int = 5
OnBegin<override>()<suspends> : void =
# Subscribe to the timer's SuccessEvent — fires when the countdown hits zero
WaveTimer.SuccessEvent.Subscribe(OnWaveTimerSuccess)
# Subscribe to the cinematic's StoppedEvent so we know when the sting ends
WaveCinematic.StoppedEvent.Subscribe(OnCinematicStopped)
# Play the opening cinematic (the cove sunrise reveal) then start wave 1
WaveCinematic.Play()
# OnCinematicStopped will kick off the first timer
# Called each time the 30-second wave countdown completes
OnWaveTimerSuccess(MaybeAgent : ?agent) : void =
set CurrentWave = CurrentWave + 1
if (CurrentWave >= MaxWaves):
# Final wave survived — play the victory cinematic and end
WaveCinematic.Play()
# OnCinematicStopped handles the game-over path
else:
# Award points to the agent who triggered the timer (if any)
if (A := MaybeAgent?):
ScoreManager.Activate(A)
else:
# No specific agent — award global points
ScoreManager.Activate()
# Play the between-wave cinematic sting
WaveCinematic.Play()
# OnCinematicStopped will restart the timer for the next wave
# Called when any cinematic sequence finishes
OnCinematicStopped(Ignored : tuple()) : void =
if (CurrentWave >= MaxWaves):
# Game over — disable the timer so nothing else fires
WaveTimer.Disable()
else:
# Restart the 30-second countdown for the next wave
WaveTimer.Reset()
WaveTimer.Start()
Line-by-line explanation
| Lines | What's happening |
|---|---|
@editable fields |
Expose the four devices to the UEFN Details panel so you can drag-and-drop the placed instances. Without @editable the Verse device can't see them. |
WaveTimer.SuccessEvent.Subscribe(OnWaveTimerSuccess) |
Hooks into the timer's completion event. The handler receives ?agent (the player who started the timer, or false if nobody did). |
WaveCinematic.StoppedEvent.Subscribe(OnCinematicStopped) |
StoppedEvent sends tuple() — no agent payload. We use it as a gate: the next wave only starts after the sting finishes. |
if (A := MaybeAgent?) |
Safe unwrap of the ?agent option. If a real player is present, award them points directly; otherwise fall back to the no-agent overload. |
ScoreManager.Activate(A) / ScoreManager.Activate() |
The two overloads of Activate — per-agent and global. The per-agent version credits the specific survivor. |
WaveTimer.Reset() then WaveTimer.Start() |
Reset snaps the countdown back to 30 s; Start begins it again. Calling Start without Reset would resume from wherever it paused. |
WaveTimer.Disable() |
Prevents any further SuccessEvent firings after the final wave. |
Common patterns
Pattern 1 — Respawn a player at the cove dock on elimination
Wire a second player_spawner_device (the respawn pad at the water's edge) and listen for players who need to re-enter the fight.
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/Patchwork }
using { /Verse.org/Simulation }
using { /Verse.org/Native }
cove_respawn_handler := class(creative_device):
@editable ShoreSpawner : player_spawner_device = player_spawner_device{}
OnBegin<override>()<suspends> : void =
# Listen for any agent that spawns from this pad
ShoreSpawner.SpawnedEvent.Subscribe(OnPlayerSpawned)
OnPlayerSpawned(Agent : agent) : void =
# Agent is a concrete agent — no unwrap needed here
# Re-enable the spawner in case it was disabled between waves
ShoreSpawner.Enable()
# Spawn the specific player at the dock location
if (P := player[Agent]):
ShoreSpawner.SpawnPlayer(P)
Pattern 2 — Increment score each time a wave starts (not just on timer success)
Use score_manager_device.Increment() to ramp up the point value as waves escalate, then Activate() to cash it out.
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/Patchwork }
using { /Verse.org/Simulation }
using { /Verse.org/Native }
cove_escalating_score := class(creative_device):
@editable ScoreManager : score_manager_device = score_manager_device{}
@editable WaveTimer : timer_device = timer_device{}
var WaveCount : int = 0
OnBegin<override>()<suspends> : void =
WaveTimer.SuccessEvent.Subscribe(OnWaveDone)
WaveTimer.Start()
OnWaveDone(MaybeAgent : ?agent) : void =
set WaveCount = WaveCount + 1
# Add one bonus point to the pool for every wave completed
ScoreManager.Increment()
# Pay out the accumulated score to the surviving agent
if (A := MaybeAgent?):
ScoreManager.Activate(A)
else:
ScoreManager.Activate()
# Reset and restart for the next wave
WaveTimer.Reset()
WaveTimer.Start()
Pattern 3 — Play a cinematic in reverse for a "wave retreats" moment
When the final wave is cleared, play the wave-crash cinematic backwards so the water visually pulls back from the dock.
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/Patchwork }
using { /Verse.org/Simulation }
using { /Verse.org/Native }
cove_wave_retreat := class(creative_device):
@editable WaveCinematic : cinematic_sequence_device = cinematic_sequence_device{}
@editable WaveTimer : timer_device = timer_device{}
var WaveCount : int = 0
MaxWaves : int = 3
OnBegin<override>()<suspends> : void =
WaveTimer.SuccessEvent.Subscribe(OnWaveDone)
WaveCinematic.StoppedEvent.Subscribe(OnCinematicStopped)
WaveTimer.Start()
OnWaveDone(MaybeAgent : ?agent) : void =
set WaveCount = WaveCount + 1
if (WaveCount >= MaxWaves):
# Play the cinematic BACKWARDS — water retreats from the dock
WaveCinematic.PlayReverse()
else:
WaveCinematic.Play()
OnCinematicStopped(Ignored : tuple()) : void =
if (WaveCount < MaxWaves):
WaveTimer.Reset()
WaveTimer.Start()
else:
WaveTimer.Disable()
Gotchas
1. ?agent must be unwrapped before use
timer_device.SuccessEvent sends ?agent, not agent. You cannot pass a ?agent directly to ScoreManager.Activate(Agent:agent). Always unwrap first:
if (A := MaybeAgent?):
ScoreManager.Activate(A)
Skipping this causes a type-mismatch compile error.
2. timer_device.Start() does NOT reset the clock
If you call Start() after the timer has already fired, it resumes from 0 (already expired). Always call Reset() before Start() when re-arming a wave timer.
3. cinematic_sequence_device.StoppedEvent sends tuple(), not agent
The handler signature must be (Ignored : tuple()) : void. Writing (Agent : agent) will compile-fail. There is no agent payload on this event.
4. score_manager_device.Activate() vs Activate(Agent)
The no-argument overload awards points globally (useful for team scores). The Agent overload credits a specific player. Make sure your Activating Team setting in the device properties matches which overload you call — a mismatch silently does nothing.
5. player_spawner_device.SpawnPlayer requires a player, not an agent
The SpawnedEvent sends agent. You must downcast: if (P := player[Agent]): before calling ShoreSpawner.SpawnPlayer(P). The cast is failable, so it must live inside an if.
6. Localized message parameters
If you ever pass text to a UI or HUD device, raw strings are not accepted. Declare a localizes helper:
MyLabel<localizes>(S : string) : message = "{S}"
There is no StringToMessage function in Verse.
7. @editable is mandatory for placed devices
Declaring var MyTimer : timer_device = timer_device{} at class scope creates a default-constructed device that is not connected to anything on your island. Only @editable fields can be wired to real placed instances in the UEFN Details panel.