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 receivestuple()— an empty tuple — not an agent. Declare the handler asOnIntroFinished(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.