Reference Verse compiles

Round-Based Flow: Building a Multi-Round Cove Battle

Every great Fortnite island moment lives inside a round — a countdown, a frantic scramble, a winner declared, then reset and go again. UEFN gives you a suite of devices — timer, score manager, cinematic sequence, player spawner, and teleporter — that you wire together in Verse to build that loop. This article teaches you how to drive a complete round-based flow from a single Verse device, set on a sun-drenched cel-shaded cove where players race across docks and clifftops each round.

Updated Examples verified on the live UEFN compiler
Watch the Knotround_based_flow in ~90 seconds.

Overview

Round-based flow is the heartbeat of competitive Fortnite islands. Each round needs:

  • A start signal — spawn players, play an intro cinematic, kick off the timer.
  • Mid-round logic — score events, damage zones, teleporters that open shortcuts.
  • An end signal — timer fires, a winner is declared, everyone is teleported back to the lobby dock.
  • A reset — scores cleared, devices re-enabled, ready for the next round.

UEFN does not ship a single "round manager" device. Instead, you compose the round loop yourself in Verse by subscribing to events from timer_device, score_manager_device, cinematic_sequence_device, player_spawner_device, and teleporter_device. This article shows you exactly how.

When to reach for this pattern:

  • You want 2–N timed rounds with automatic resets.
  • You need score events to drive round outcomes (first to X points wins).
  • You want a cinematic intro/outro per round.
  • You want to teleport all players to a new arena each round.

API Reference

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

Walkthrough

The Scenario

Sunny Cove Dash — a 2D cel-shaded island. Players spawn on a wooden dock, race across clifftop platforms, and score points by reaching the far shore. Each round lasts 60 seconds. The player who reaches the score cap first wins the round; after 3 rounds the game ends. Between rounds a cinematic sequence plays (seagulls, wave crash) and everyone is teleported back to the starting dock.

Device Setup in UEFN Editor

Place and assign these devices in your level, then link them to the Verse device's @editable fields:

Field Device Type Purpose
RoundTimer timer_device 60-second countdown per round
ScoreManager score_manager_device Awards points when a player reaches the shore
IntroSequence cinematic_sequence_device Plays the wave-crash intro
DockSpawner player_spawner_device Spawns players on the dock
LobbyTeleporter teleporter_device Sends everyone back to the dock between rounds

Complete Verse Device

using { /Fortnite.com/Devices }
using { /Fortnite.com/Game }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/UI }

# Sunny Cove Dash — round-based flow manager
cove_round_manager := class(creative_device):

    # ── Editable device references ──────────────────────────────────
    @editable RoundTimer     : timer_device              = timer_device{}
    @editable ScoreManager   : score_manager_device      = score_manager_device{}
    @editable IntroSequence  : cinematic_sequence_device = cinematic_sequence_device{}
    @editable DockSpawner    : player_spawner_device     = player_spawner_device{}
    @editable LobbyTeleporter: teleporter_device         = teleporter_device{}

    # ── Round state ─────────────────────────────────────────────────
    var CurrentRound  : int = 0
    MaxRounds         : int = 3

    # ── Entry point ─────────────────────────────────────────────────
    OnBegin<override>()<suspends>:void =
        # Subscribe to timer events
        RoundTimer.SuccessEvent.Subscribe(OnRoundTimerSuccess)
        RoundTimer.FailureEvent.Subscribe(OnRoundTimerFailure)

        # Subscribe to score events — fires each time a player scores
        ScoreManager.ScoreOutputEvent.Subscribe(OnPlayerScored)

        # Subscribe to the intro cinematic finishing so we can start the timer
        RoundTimer.SuccessEvent.Subscribe(OnRoundTimerSuccess)  # already subscribed above

        # Subscribe to teleporter — log when a player arrives at the dock
        LobbyTeleporter.TeleportedEvent.Subscribe(OnPlayerTeleportedToDock)

        # Kick off the first round
        RunRound()

    # ── Core round loop ─────────────────────────────────────────────
    RunRound()<suspends>:void =
        set CurrentRound = CurrentRound + 1
        if (CurrentRound > MaxRounds):
            # All rounds done — game over
            EndGame()
            return

        # 1. Play the cel-shaded wave-crash intro cinematic
        IntroSequence.Play()

        # 2. Reset score manager so everyone starts at 0 this round
        ScoreManager.Reset()

        # 3. Reset and start the 60-second round timer
        RoundTimer.Reset()
        RoundTimer.Start()

        # 4. Wait for the timer to complete (success = time ran out normally)
        #    The timer's SuccessEvent handler (OnRoundTimerSuccess) will
        #    call the next round after a short break.

    # ── Timer fired — round ended by timeout ────────────────────────
    OnRoundTimerSuccess(MaybeAgent : ?agent):void =
        # Teleport all players back to the dock for the between-round break
        TeleportAllToDock()
        # Disable scoring during the break
        ScoreManager.Disable()
        # Chain into the next round after a beat
        spawn { WaitThenNextRound() }

    # ── Timer failure — something went wrong, still advance ─────────
    OnRoundTimerFailure(MaybeAgent : ?agent):void =
        TeleportAllToDock()
        ScoreManager.Disable()
        spawn { WaitThenNextRound() }

    # ── A player reached the shore and scored ───────────────────────
    OnPlayerScored(ScoringAgent : agent):void =
        # Re-enable the score manager for the next scorer
        ScoreManager.Enable()

    # ── A player arrived back at the dock ───────────────────────────
    OnPlayerTeleportedToDock(Arriving : agent):void =
        # Respawn them cleanly on the dock spawner
        if (P := player[Arriving]):
            DockSpawner.SpawnPlayer(P)

    # ── Wait a moment, then start the next round ────────────────────
    WaitThenNextRound()<suspends>:void =
        Sleep(5.0)  # 5-second between-round break
        ScoreManager.Enable()
        RunRound()

    # ── Send every player to the lobby teleporter ───────────────────
    TeleportAllToDock():void =
        var Players : []player = GetPlayspace().GetPlayers()
        for (P : Players):
            LobbyTeleporter.Teleport(P)

    # ── All rounds complete ─────────────────────────────────────────
    EndGame():void =
        # Play the outro cinematic in reverse (sun setting over the cove)
        IntroSequence.PlayReverse()
        RoundTimer.Disable()
        ScoreManager.Disable()

Line-by-Line Explanation

@editable fields — Every device reference must be declared as an @editable field at class scope. You then drag the placed devices from the Outliner into these slots in the Details panel. Without this, Verse cannot find the devices.

OnBegin — The <suspends> effect lets OnBegin Sleep and Await concurrently. All event subscriptions happen here before RunRound() is called.

RunRound() — The core loop. It increments CurrentRound, plays the intro cinematic with IntroSequence.Play(), resets the score manager with ScoreManager.Reset(), resets and starts the timer with RoundTimer.Reset() then RoundTimer.Start(). The timer runs asynchronously; when it fires SuccessEvent, OnRoundTimerSuccess is called.

OnRoundTimerSuccess / OnRoundTimerFailure — Both receive a ?agent (the agent who last interacted with the timer, or false). Both call TeleportAllToDock() and chain into WaitThenNextRound() via spawn.

TeleportAllToDock() — Calls LobbyTeleporter.Teleport(P) for every player. Teleport moves a player directly to this device's location — useful for a forced reset.

OnPlayerTeleportedToDock — Listens to LobbyTeleporter.TeleportedEvent (an agent event). Casts to player and calls DockSpawner.SpawnPlayer(P) to place them cleanly on the dock.

EndGame() — Calls IntroSequence.PlayReverse() for a sunset outro, then disables the timer and score manager.


Common Patterns

Pattern 1 — Damage Volume Raises the Stakes Mid-Round

Add a rising-tide damage volume on the cove's lower dock. Each round it activates earlier, forcing players upward onto the clifftop path.

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

# Activates a damage volume when the round timer enters urgency mode
cove_tide_hazard := class(creative_device):

    @editable RoundTimer  : timer_device         = timer_device{}
    @editable TideVolume  : damage_volume_device  = damage_volume_device{}

    OnBegin<override>()<suspends>:void =
        # Disable the tide at the start — it only rises in urgency mode
        TideVolume.SetDamage(0)

        # Subscribe to the urgency mode event (e.g., last 15 seconds)
        RoundTimer.StartUrgencyModeEvent.Subscribe(OnUrgencyStarted)

        # When the round timer succeeds, disable the tide
        RoundTimer.SuccessEvent.Subscribe(OnRoundEnd)

    OnUrgencyStarted(MaybeAgent : ?agent):void =
        # Ramp up tide damage — 20 damage per tick
        TideVolume.SetDamage(20)

    OnRoundEnd(MaybeAgent : ?agent):void =
        TideVolume.SetDamage(0)

What this covers: timer_device.StartUrgencyModeEvent, damage_volume_device.SetDamage(). The tide only bites when the clock is screaming.


Pattern 2 — Score Manager Increment Combo

Players who reach the far shore without taking damage get a bonus point. This pattern uses ScoreManager.Increment() before ScoreManager.Activate() to award 2 points instead of 1.

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

# Awards bonus points to players who arrive at the shore unscathed
cove_bonus_scorer := class(creative_device):

    @editable ShoreVolume  : damage_volume_device  = damage_volume_device{}
    @editable ScoreManager : score_manager_device   = score_manager_device{}

    # Track which players took damage this round
    var DamagedPlayers : []agent = array{}

    OnBegin<override>()<suspends>:void =
        ShoreVolume.AgentEntersEvent.Subscribe(OnAgentReachesShore)
        ShoreVolume.AgentExitsEvent.Subscribe(OnAgentExitsShore)

    OnAgentReachesShore(Arriving : agent):void =
        # Check if this player took no damage (not in DamagedPlayers)
        var TookDamage : logic = false
        for (D : DamagedPlayers):
            if (D = Arriving):
                set TookDamage = true
        if (TookDamage = false):
            # Bonus round: increment then activate = 2 points
            ScoreManager.Increment(Arriving)
            ScoreManager.Activate(Arriving)
        else:
            # Standard 1 point
            ScoreManager.Activate(Arriving)

    OnAgentExitsShore(Leaving : agent):void =
        # Nothing needed on exit for this pattern
        ScoreManager.Enable()

What this covers: score_manager_device.Increment(Agent), score_manager_device.Activate(Agent), damage_volume_device.AgentEntersEvent, damage_volume_device.AgentExitsEvent.


Pattern 3 — Cinematic Sequence Controls the Round Gate

The round does not start until the intro cinematic finishes. Use StoppedEvent on the cinematic to gate the timer start.

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

# Gates the round timer behind the intro cinematic finishing
cove_cinematic_gate := class(creative_device):

    @editable IntroSequence : cinematic_sequence_device = cinematic_sequence_device{}
    @editable RoundTimer    : timer_device              = timer_device{}
    @editable DockSpawner   : player_spawner_device     = player_spawner_device{}

    OnBegin<override>()<suspends>:void =
        # Subscribe to the cinematic stopped event
        IntroSequence.StoppedEvent.Subscribe(OnIntroFinished)

        # Subscribe to spawner so we know when a player is ready
        DockSpawner.SpawnedEvent.Subscribe(OnPlayerSpawned)

        # Play the intro — timer will not start until StoppedEvent fires
        IntroSequence.Play()

    OnPlayerSpawned(SpawnedAgent : agent):void =
        # Could track readiness here; for now just log via score
        # (no Print — use a score ping instead)
        ScoreManager_Placeholder():void = {}

    OnIntroFinished(Unused : tuple()):void =
        # Cinematic done — now start the round timer
        RoundTimer.Reset()
        RoundTimer.Start()

What this covers: cinematic_sequence_device.StoppedEvent (which sends tuple() — note the handler signature), cinematic_sequence_device.Play(), timer_device.Reset(), timer_device.Start(), player_spawner_device.SpawnedEvent.

Note on StoppedEvent: Its handler receives tuple() — an empty tuple — not an agent. Declare the handler as OnIntroFinished(Unused : tuple()):void.


Gotchas

1. ?agent Events Must Be Unwrapped

timer_device.SuccessEvent and FailureEvent send ?agent (an optional agent). If you try to use the value directly as an agent the compiler will reject it. Always unwrap:

OnRoundTimerSuccess(MaybeAgent : ?agent):void =
    if (A := MaybeAgent?):
        # A is a real agent here
        ScoreManager.Activate(A)
    # else: no agent triggered it (e.g., time simply ran out)

2. cinematic_sequence_device.StoppedEvent Sends tuple(), Not an Agent

Unlike most device events, StoppedEvent sends an empty tuple. Your handler must match:

OnIntroFinished(Unused : tuple()):void =
    RoundTimer.Start()

If you write (Agent : agent) the subscribe call will not compile.

3. @editable Is Non-Negotiable

You cannot write var T : timer_device = timer_device{} at class scope and expect it to reference a placed device. Only @editable fields are wired to placed devices via the Details panel. A bare timer_device{} is an unplaced default instance — calling Start() on it does nothing visible in-game.

4. ScoreManager.Reset() vs ScoreManager.Reset(Agent)

There are two overloads. Reset() (no argument) resets the device's trigger count for all players. Reset(Agent) resets only for that specific agent. For a full round reset, call the no-argument version.

5. player_spawner_device.SpawnPlayer Requires a player, Not an agent

SpawnPlayer takes a player, not the broader agent type. If you receive an agent from an event, cast it first:

if (P := player[SomeAgent]):
    DockSpawner.SpawnPlayer(P)

Without the cast, the compiler will report a type mismatch.

6. spawn vs Await for Chaining Async Rounds

Event handlers are not <suspends> — they return void synchronously. To chain into async logic (like Sleep then RunRound), use spawn { MyAsyncFunc() } inside the handler. Do not call a <suspends> function directly from a non-suspending handler.

7. damage_volume_device.SetDamage Is Clamped 1–500

Passing 0 to SetDamage is clamped to 1 — the volume will still deal 1 damage per tick. To truly disable the volume mid-round, call TideVolume.Disable() (inherited from effect_volume_device) rather than setting damage to zero.

Build your own lesson with round_based_flow

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 →