Reference Verse compiles

Treasure Cove Phases: the game state machine

Round-based games live and die by their game loop: wait, count down, play, celebrate, reset, repeat. A state machine turns that loop into one enum, one `var`, and a handful of small async phase functions — each of which runs its phase, waits for its exit event, and returns the next phase. In this lesson you build the master state machine for the Coastal Race: five phases, event-driven transitions, and a `race` expression that gives the Racing phase two competing exits (finish line → Podium, storm timeout → Reset). It grows directly out of the enum seed you planted on South Shores.

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

Overview

A simple game state machine is not a single UEFN device you drag from the content browser — it is a design pattern you implement in Verse. You define an enum (or a set of named states) and a var that holds the current state, then drive your real devices (timer_device, teleporter_device, score_manager_device, cinematic_sequence_device, etc.) in response to state changes.

When should you reach for this pattern?

  • Your island has more than two phases (lobby → gameplay → results, for example).
  • Multiple devices need to turn on or off together when a phase changes.
  • You want a single source of truth so you never accidentally start the timer while the game is already in the results screen.

The coastal race scenario below has three states:

State What's happening on the island
Lobby Players are on the dock; spawners are active; race teleporter is disabled
Race Players are on the shore path; countdown timer is running; score manager is live
Finish Players are at the clifftop; cinematic plays; scores are awarded

API Reference

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

Walkthrough

Island setup (place these devices in UEFN)

Device Role
player_spawner_device Spawns players at the dock
timer_device 60-second race countdown
teleporter_device (×2) StartTeleporter sends players to shore; FinishTeleporter sends them to clifftop
score_manager_device Awards points to the first finisher
cinematic_sequence_device Clifftop victory cutscene

Name each device in the Outliner so you can assign it to the @editable fields below.

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

# ---------------------------------------------------------------------------
# Enum-style constants for the three race phases.
# Verse doesn't have a built-in enum keyword at this level, so we use an
# int alias pattern — simple and readable.
# ---------------------------------------------------------------------------
lobby_state  : int = 0
race_state   : int = 1
finish_state : int = 2

coastal_race_manager := class(creative_device):

    # -----------------------------------------------------------------------
    # Editable device references — assign these in the UEFN Details panel.
    # -----------------------------------------------------------------------
    @editable DockSpawner      : player_spawner_device     = player_spawner_device{}
    @editable RaceTimer        : timer_device              = timer_device{}
    @editable StartTeleporter  : teleporter_device         = teleporter_device{}
    @editable FinishTeleporter : teleporter_device         = teleporter_device{}
    @editable ScoreManager     : score_manager_device      = score_manager_device{}
    @editable VictoryCinematic : cinematic_sequence_device = cinematic_sequence_device{}

    # -----------------------------------------------------------------------
    # Mutable state — starts in Lobby.
    # -----------------------------------------------------------------------
    var CurrentState : int = lobby_state

    # -----------------------------------------------------------------------
    # Entry point.
    # -----------------------------------------------------------------------
    OnBegin<override>()<suspends> : void =
        # Wire up events before entering the lobby phase.
        RaceTimer.SuccessEvent.Subscribe(OnRaceTimerSuccess)
        RaceTimer.FailureEvent.Subscribe(OnRaceTimerFailure)
        FinishTeleporter.TeleportedEvent.Subscribe(OnPlayerReachedClifftop)

        EnterLobby()

    # -----------------------------------------------------------------------
    # PHASE 1 — Lobby: players gather at the dock.
    # -----------------------------------------------------------------------
    EnterLobby() : void =
        set CurrentState = lobby_state

        # Make sure the race devices are off while we're in the lobby.
        RaceTimer.Disable()
        StartTeleporter.Disable()
        FinishTeleporter.Disable()
        ScoreManager.Disable()

        # Dock spawner is the only active spawn point.
        DockSpawner.Enable()

    # -----------------------------------------------------------------------
    # PHASE 2 — Race: called externally (e.g. from a button Verse device)
    # or after a lobby countdown. Exposed as public so a lobby button can
    # call coastal_race_manager_instance.BeginRace().
    # -----------------------------------------------------------------------
    BeginRace<public>() : void =
        if (CurrentState = lobby_state):
            set CurrentState = race_state

            # Lock the dock; open the shore path.
            DockSpawner.Disable()
            StartTeleporter.Enable()
            FinishTeleporter.Enable()
            ScoreManager.Enable()

            # Start the 60-second countdown (configured in the device panel).
            RaceTimer.Start()

    # -----------------------------------------------------------------------
    # PHASE 3 — Finish: a player stepped through the finish teleporter.
    # -----------------------------------------------------------------------
    OnPlayerReachedClifftop(Agent : agent) : void =
        if (CurrentState = race_state):
            set CurrentState = finish_state

            # Stop the timer so it doesn't fire FailureEvent.
            RaceTimer.Pause()

            # Award points to the first finisher.
            ScoreManager.Activate(Agent)

            # Play the clifftop victory cinematic for everyone.
            VictoryCinematic.Play()

            # After the cinematic, loop back to lobby.
            # (Subscribe to StoppedEvent to know when it ends.)
            VictoryCinematic.StoppedEvent.Subscribe(OnCinematicStopped)

    # -----------------------------------------------------------------------
    # Timer ran out before anyone reached the clifftop — race failed.
    # -----------------------------------------------------------------------
    OnRaceTimerFailure(MaybeAgent : ?agent) : void =
        if (CurrentState = race_state):
            set CurrentState = finish_state
            FinishTeleporter.Disable()
            # Teleport everyone back to the dock to try again.
            EnterLobby()

    # -----------------------------------------------------------------------
    # Timer hit its success threshold (configured in device panel).
    # -----------------------------------------------------------------------
    OnRaceTimerSuccess(MaybeAgent : ?agent) : void =
        # In this design, success means the race clock hit zero with a winner
        # already declared — nothing extra needed here.
        # (The OnPlayerReachedClifftop handler already moved us to finish_state.)
        block:
            false

    # -----------------------------------------------------------------------
    # Cinematic finished — return to lobby for the next round.
    # -----------------------------------------------------------------------
    OnCinematicStopped(Unused : tuple()) : void =
        EnterLobby()```

### Line-by-line highlights

- **`var CurrentState : int`**  the single source of truth. Every phase-change method checks this first, so overlapping events (e.g. two players hitting the finish at the same time) are safely ignored after the first.
- **`EnterLobby()` / `BeginRace()` / `OnPlayerReachedClifftop()`**  each method owns one state transition. Devices are enabled/disabled *inside* the transition, not scattered across handlers.
- **`RaceTimer.Start()`**  starts the `timer_device` ticking. `SuccessEvent` and `FailureEvent` are both subscribed so both outcomes are handled.
- **`ScoreManager.Activate(Agent)`**  awards the configured point value to the first finisher.
- **`VictoryCinematic.Play()`**  fires the cinematic sequence for all players; `StoppedEvent` drives the return to lobby.
- **`OnRaceTimerFailure(MaybeAgent : ?agent)`**  the `?agent` is an option type; we don't need to unwrap it here because we only care that the timer fired, not *who* triggered it.

---

## Common patterns

### Pattern 1 — Guard a state transition with `decides`

Use a failable helper to make state guards readable and reusable.

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

lobby_state  : int = 0
race_state   : int = 1
finish_state : int = 2

state_guard_example := class(creative_device):

    @editable RaceTimer   : timer_device         = timer_device{}
    @editable StartTele   : teleporter_device     = teleporter_device{}

    var CurrentState : int = lobby_state

    # Failable helper — succeeds only when we are in the expected state.
    IsInState(Expected : int)<decides> : void =
        CurrentState = Expected

    OnBegin<override>()<suspends> : void =
        # Only start the timer if we're actually in the lobby.
        if (IsInState(lobby_state)):
            set CurrentState = race_state
            StartTele.Enable()
            RaceTimer.Start()

Pattern 2 — Teleport a player to the shore on a timer tick

Subscribe to SuccessEvent and use the unwrapped ?agent to teleport only the triggering player.

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

shore_teleport_on_success := class(creative_device):

    @editable CountdownTimer  : timer_device    = timer_device{}
    @editable ShoreTeleporter : teleporter_device = teleporter_device{}

    OnBegin<override>()<suspends> : void =
        CountdownTimer.SuccessEvent.Subscribe(OnTimerSuccess)

    # SuccessEvent sends ?agent — must unwrap before use.
    OnTimerSuccess(MaybeAgent : ?agent) : void =
        if (A := MaybeAgent?):
            # Teleport the specific agent who triggered the timer.
            ShoreTeleporter.Activate(A)
        else:
            # No specific agent — teleport is skipped; handle globally elsewhere.
            ShoreTeleporter.Enable()

Pattern 3 — Award bonus points and play a cinematic for the cove checkpoint

Combine score_manager_device.Increment() with cinematic_sequence_device.Play(Agent) for a per-player checkpoint reward.

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

cove_checkpoint := class(creative_device):

    @editable CheckpointTeleporter : teleporter_device         = teleporter_device{}
    @editable BonusScorer          : score_manager_device      = score_manager_device{}
    @editable CheckpointCinematic  : cinematic_sequence_device = cinematic_sequence_device{}

    OnBegin<override>()<suspends> : void =
        CheckpointTeleporter.TeleportedEvent.Subscribe(OnPlayerPassedCove)

    OnPlayerPassedCove(Agent : agent) : void =
        # Increment the score bucket, then award it immediately.
        BonusScorer.Increment(Agent)
        BonusScorer.Activate(Agent)

        # Play the cove checkpoint cinematic for this specific player.
        CheckpointCinematic.Play(Agent)

Gotchas

1. State re-entrancy — always guard transitions

If two players hit the finish teleporter within the same tick, OnPlayerReachedClifftop fires twice. Without the if (CurrentState = race_state) guard, you'd award points twice and play the cinematic twice. Always check CurrentState at the top of every handler.

2. ?agent events must be unwrapped

timer_device.SuccessEvent and FailureEvent send ?agent (an option), not agent. You cannot pass MaybeAgent directly to ScoreManager.Activate(). Unwrap first:

if (A := MaybeAgent?):
    ScoreManager.Activate(A)

3. timer_device.Start() vs timer_device.Enable()

Enable() makes the device receptive to signals; Start() actually begins the countdown. You need both if the device starts disabled. Call Enable() then Start(), or configure the device to start enabled and just call Start().

4. cinematic_sequence_device.StoppedEvent sends tuple()

The stopped event handler signature is (Unused : tuple()) : void, not () : void and not (Agent : agent). Getting this wrong causes a compile error.

5. score_manager_device team restrictions

If your Score Manager device's Activating Team property is set to a specific team in the UEFN panel, you must use the Agent overloads (Activate(Agent), Enable(Agent), etc.). The no-argument overloads will silently do nothing for players on the wrong team.

6. @editable fields must be assigned in the Details panel

Declaring @editable MyTimer : timer_device = timer_device{} gives you a default empty device at compile time, but it won't reference your placed device until you drag the placed device into the field in the UEFN Details panel. Forgetting this is the #1 cause of "my device does nothing" bugs.

7. No auto int↔float conversion

If you compare or assign state values, keep them all int. Verse will not silently cast 0.0 to 0 — mixing types causes a type error at compile time.

Build your own lesson with game_state_machine

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 →