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

  1. Place three creature_spawner_device instances near the dock — name them Wave1Spawner, Wave2Spawner, Wave3Spawner.
  2. Place one trigger_device called WaveStartTrigger — set Max Trigger Count to 1 in its properties so players can only kick off the run once.
  3. Place one trigger_device called TreasureUnlockedTrigger — wire it to whatever victory logic you like (e.g. an End Game device).
  4. 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 lesson
Pass the quiz above to chart this quest in your Journal.

Sources

/guides/spawn-wave-pattern
Source

Biloxi Studios original lesson

Guided course

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