Wave Director — the Spawn-Wave Pattern
In this lesson you'll learn to
- ✓Student can orchestrate timed escalating spawn waves in Verse (wave counter, per-wave size/delay tables, wave-cleared detection).
Student can orchestrate timed escalating spawn waves in Verse (wave counter, per-wave size/delay tables, wave-cleared detection).
📖 Reference & full walkthrough: Wave Director — the Spawn-Wave Pattern
🔁 Builds on: north-jungle: arrays (wave tables) + events; verse-npc-spawner-waves companion
🧩 Your capstone piece: The wave director loop that IS the game's spine (Wave 1..N escalation)
Overview
The spawn wave pattern is the backbone of any horde or wave-defence game mode. You place one or more creature_spawner_device instances on your island, then use a trigger_device (or a chain of them) to gate each wave. Between waves you can pause, reward players, or ramp up difficulty by changing spawn limits or reset delays — all from Verse.
When to reach for it:
- A pirate cove where skeleton waves guard a treasure chest.
- A survival mode where each eliminated wave unlocks the next.
- Any scenario where you want discrete, scripted enemy surges rather than a continuous trickle.
The key devices are:
| Device | Role |
|---|---|
creature_spawner_device |
Spawns creatures, fires SpawnedEvent / EliminatedEvent, exposes GetSpawnLimit |
trigger_device |
Acts as a wave-start gate; fires TriggeredEvent, supports SetMaxTriggerCount / SetResetDelay |
API Reference
creature_spawner_device
Used to spawn one or more waves of creatures of customizable types at selected time intervals.
Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse).
creature_spawner_device<public> := class<concrete><final>(creative_device_base):
Events (subscribe a handler to react):
| Event | Signature | Description |
|---|---|---|
SpawnedEvent |
SpawnedEvent<public>:listenable(agent) |
Signaled when a creature is spawned. Sends the agent creature who was spawned. |
EliminatedEvent |
EliminatedEvent<public>:listenable(device_ai_interaction_result) |
Signaled when a creature is eliminated. Source is the agent that has eliminated the creature. If the creature was eliminated by a non-agent then Source is 'false'. Target is the creature that was eliminated. |
Methods (call these to make the device act):
| Method | Signature | Description |
|---|---|---|
SpawnAt |
SpawnAt<public>(Position:(/Verse.org/SpatialMath:)vector3, ?Rotation:?(/Verse.org/SpatialMath:)rotation |
Spawn a creature at the given position. When Rotation is not provided, it will default to the Device's rotation. Returns the agent spawned or false if the device has reached its maximum spawn count. This function is <suspends> because it |
Enable |
Enable<public>():void |
Enables this device. |
Disable |
Disable<public>():void |
Disables this device. |
DestroySpawner |
DestroySpawner<public>():void |
Destroys this device. |
EliminateCreatures |
EliminateCreatures<public>():void |
Eliminates all creatures spawned by this device. |
GetSpawnLimit |
GetSpawnLimit<public>()<transacts>:int |
Returns the spawn limit of the device. |
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. |
player
Full public surface, resolved verbatim from the live Epic digest (Verse.digest.verse). Inherited members are merged from agent.
player<native><public> := class<unique><persistent><module_scoped_var_weak_map_key><epic_internal>(agent):
Walkthrough
Scenario — Skeleton Cove: Players land on a sun-bleached dock. Three waves of skeleton pirates defend the treasure. Each wave starts when the previous one is fully eliminated. After all three waves the treasure trigger fires and the round ends.
Setup in UEFN Editor
- Place three
creature_spawner_deviceinstances near the dock — name themWave1Spawner,Wave2Spawner,Wave3Spawner. - Place one
trigger_devicecalledWaveStartTrigger— set Max Trigger Count to1in its properties so players can only kick off the run once. - Place one
trigger_devicecalledTreasureUnlockedTrigger— wire it to whatever victory logic you like (e.g. an End Game device). - Drag your Verse device onto the island and assign all four devices in the Details panel.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/SpatialMath }
# ---------------------------------------------------------------
# skeleton_cove_wave_manager
# Drives three escalating skeleton waves on a sunny pirate dock.
# Wire WaveStartTrigger, Wave1/2/3Spawner, TreasureUnlockedTrigger
# in the Details panel before playtesting.
# ---------------------------------------------------------------
skeleton_cove_wave_manager := class(creative_device):
# ── Editable device references ──────────────────────────────
@editable
WaveStartTrigger : trigger_device = trigger_device{}
@editable
Wave1Spawner : creature_spawner_device = creature_spawner_device{}
@editable
Wave2Spawner : creature_spawner_device = creature_spawner_device{}
@editable
Wave3Spawner : creature_spawner_device = creature_spawner_device{}
@editable
TreasureUnlockedTrigger : trigger_device = trigger_device{}
# ── Mutable wave state ──────────────────────────────────────
var CurrentWave : int = 0
var EliminatedCount : int = 0
var WaveLimit : int = 0
# ── Lifecycle ───────────────────────────────────────────────
OnBegin<override>()<suspends> : void =
# Only Wave1Spawner is active at start; disable the others
Wave2Spawner.Disable()
Wave3Spawner.Disable()
# Players can only start the run once
WaveStartTrigger.SetMaxTriggerCount(1)
# Subscribe to the start trigger
WaveStartTrigger.TriggeredEvent.Subscribe(OnWaveStartTriggered)
# Subscribe to eliminations on all three spawners
Wave1Spawner.EliminatedEvent.Subscribe(OnCreatureEliminated)
Wave2Spawner.EliminatedEvent.Subscribe(OnCreatureEliminated)
Wave3Spawner.EliminatedEvent.Subscribe(OnCreatureEliminated)
# ── Handlers ────────────────────────────────────────────────
# Called when a player steps on the WaveStartTrigger pressure plate
OnWaveStartTriggered(MaybeAgent : ?agent) : void =
spawn { RunWaves() }
# Called every time any creature is eliminated
OnCreatureEliminated(Result : device_ai_interaction_result) : void =
set EliminatedCount += 1
# Wave is cleared when every creature in it has been eliminated
if (EliminatedCount >= WaveLimit):
spawn { AdvanceWave() }
# ── Async wave logic ────────────────────────────────────────
RunWaves()<suspends> : void =
# ── Wave 1: 3 skeletons on the dock ──
set CurrentWave = 1
set EliminatedCount = 0
Wave1Spawner.Enable()
set WaveLimit = Wave1Spawner.GetSpawnLimit()
# Wait until AdvanceWave signals Wave 2 should start
# (AdvanceWave enables Wave2Spawner when CurrentWave = 1)
AdvanceWave()<suspends> : void =
if (CurrentWave = 1):
# ── Wave 2: heavier crew from the ship ──
set CurrentWave = 2
set EliminatedCount = 0
Wave1Spawner.EliminateCreatures() # clean up any stragglers
Wave2Spawner.Enable()
set WaveLimit = Wave2Spawner.GetSpawnLimit()
else if (CurrentWave = 2):
# ── Wave 3: the captain's guard ──
set CurrentWave = 3
set EliminatedCount = 0
Wave2Spawner.EliminateCreatures()
Wave3Spawner.Enable()
set WaveLimit = Wave3Spawner.GetSpawnLimit()
else if (CurrentWave = 3):
# ── All waves cleared — open the treasure ──
set CurrentWave = 0
Wave3Spawner.EliminateCreatures()
TreasureUnlockedTrigger.Trigger() # fires victory logic
Line-by-line highlights
| Lines | What's happening |
|---|---|
Wave2Spawner.Disable() / Wave3Spawner.Disable() |
Prevent later waves from auto-spawning before their turn. |
WaveStartTrigger.SetMaxTriggerCount(1) |
Locks the start gate to one use so players can't restart mid-run. |
Wave1Spawner.EliminatedEvent.Subscribe(...) |
Listens for every skeleton death across all three spawners. |
Wave1Spawner.GetSpawnLimit() |
Reads the spawn cap you set in the device's Details panel — no magic numbers in code. |
Wave1Spawner.EliminateCreatures() |
Clears any stragglers before the next wave so the count stays clean. |
TreasureUnlockedTrigger.Trigger() |
Fires the victory trigger without needing a player agent reference. |
Common patterns
Pattern 1 — Spawn a creature at a specific clifftop position
Use SpawnAt to place a boss skeleton exactly on the clifftop lookout, regardless of where the spawner device sits on the island.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/SpatialMath }
clifftop_boss_spawner := class(creative_device):
@editable
BossSpawner : creature_spawner_device = creature_spawner_device{}
@editable
BossTrigger : trigger_device = trigger_device{}
OnBegin<override>()<suspends> : void =
BossTrigger.TriggeredEvent.Subscribe(OnBossTriggered)
OnBossTriggered(MaybeAgent : ?agent) : void =
spawn { SpawnBossOnCliff() }
SpawnBossOnCliff()<suspends> : void =
# World-space position of the clifftop lookout (set in editor)
CliffPosition : vector3 = vector3{X := 2400.0, Y := -800.0, Z := 620.0}
# SpawnAt returns ?agent — ignore result here, just fire and forget
BossSpawner.SpawnAt(CliffPosition)
Key point: SpawnAt is <suspends> — it must be called from an async context, so we spawn a helper function from the event handler.
Pattern 2 — Limit and reset the wave-start trigger for a timed retry
Give players a 10-second cooldown before they can restart a failed wave, and cap retries at three attempts.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
wave_retry_gate := class(creative_device):
@editable
RetryTrigger : trigger_device = trigger_device{}
@editable
WaveSpawner : creature_spawner_device = creature_spawner_device{}
OnBegin<override>()<suspends> : void =
# Players get 3 attempts; 0 = unlimited, 1-20 = capped
RetryTrigger.SetMaxTriggerCount(3)
# 10-second cooldown between attempts
RetryTrigger.SetResetDelay(10.0)
# Half-second delay before the trigger tells other devices it fired
RetryTrigger.SetTransmitDelay(0.5)
RetryTrigger.TriggeredEvent.Subscribe(OnRetry)
OnRetry(MaybeAgent : ?agent) : void =
# Show remaining attempts in the output log (dev only)
Remaining := RetryTrigger.GetTriggerCountRemaining()
WaveSpawner.Enable()
Key point: GetTriggerCountRemaining() returns 0 when GetMaxTriggerCount() is unlimited — check GetMaxTriggerCount() first if you need to distinguish the two cases.
Pattern 3 — React to who eliminated a creature (agent unwrap)
Award a bonus to the specific player who lands the killing blow on a pirate captain.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
kill_credit_tracker := class(creative_device):
@editable
CaptainSpawner : creature_spawner_device = creature_spawner_device{}
@editable
BonusTrigger : trigger_device = trigger_device{}
OnBegin<override>()<suspends> : void =
CaptainSpawner.EliminatedEvent.Subscribe(OnCaptainEliminated)
OnCaptainEliminated(Result : device_ai_interaction_result) : void =
# Result.Source is ?agent — false when a non-agent killed the creature
if (Killer := Result.Source?):
# Fire the bonus trigger on behalf of the killing player
BonusTrigger.Trigger(Killer)
else:
# Non-agent kill (e.g. fall damage) — trigger without agent
BonusTrigger.Trigger()
Key point: EliminatedEvent sends a device_ai_interaction_result, not a bare agent. Always read .Source? and unwrap it with if (Killer := Result.Source?): before passing it anywhere that needs an agent.
Gotchas
1. SpawnAt is <suspends> — never call it directly from a sync handler
EliminatedEvent and TriggeredEvent handlers are synchronous. Calling SpawnAt directly inside them will fail to compile. Always wrap it: spawn { MyAsyncSpawnFunction() }.
2. GetSpawnLimit() reflects the device setting, not live creature count
GetSpawnLimit() returns the cap you configured in the Details panel. It does not count how many creatures are currently alive. Track eliminations yourself with a var counter.
3. SetMaxTriggerCount clamps to [0, 20]
Passing 21 or higher silently clamps to 20. Use 0 for unlimited. Plan your wave count accordingly.
4. EliminatedEvent.Source is ?agent, not agent
The source of a kill can be false (non-agent: environment, fall damage, another creature). Always unwrap: if (Killer := Result.Source?): before using it as an agent.
5. Disable unused spawners at OnBegin
If a creature_spawner_device is set to Spawn on Start in its properties, it will fire the moment the game begins — before your Verse code runs. Disable wave 2 and wave 3 spawners in OnBegin to prevent premature spawning.
6. EliminateCreatures() is instant — subscribe before you call it
If you subscribe to EliminatedEvent after calling EliminateCreatures(), you will miss those elimination signals. Always subscribe in OnBegin, before any wave logic runs.
7. Localized text for on-screen messages
If you ever display wave info via a UI device that takes a message param, you cannot pass a raw string. Declare a localizer: WaveText<localizes>(S : string) : message = "{S}" and call WaveText("Wave 2 incoming!").
Keep going — free
You've read the intro. The rest of this lesson is free for members. Sign in to continue and track your progress.
Sign in free to continue this lessonSources
/guides/spawn-wave-patternBiloxi Studios original lesson
Add this lesson to your free study plan.
🧭 The Keeper’s log
Quest complete? Chart your next heading from the 🐉 East Volcano expedition.
⛵ Take me there