Every good beach game has a rhythm: everyone gathers, the whistle blows, chaos happens, and then somebody gets crowned. In code, that rhythm is a state machine — and the simplest, sturdiest way to build one in Verse is an enum stored in a var. In this lesson you will define the Shell Hunt's three phases, switch behavior per phase, and attach data (labels, timings) to each phase without a single magic number.
What you will build
The LOBBY -> HUNTING -> CELEBRATION phase machine that the entire South Shores Shell Hunt capstone hangs off. Every handler in the finished minigame — shell pickups, the round timer, the scoreboard — will start by asking one question: "what phase are we in?" This device is that answer:
- A
shell_hunt_phaseenum with exactly three legal states. - A
var CurrentPhasethat is the single source of truth for the round. - Two buttons that request transitions — and get ignored when the request is illegal (pressing "Turn In Shells" in the Lobby does nothing, on purpose).
- Per-phase data (banner text, banner duration) returned by tiny
casehelpers — the enum-with-data pattern.
Coral the flamingo's rule of claw: the phase var changes in exactly one place. Everything else just reads it.
Walkthrough
Step 1 — Name the phases. An enum is a fixed menu of named values. You can write it inline as shell_hunt_phase := enum{Lobby, Hunting, Celebration} or in block form (used below — friendlier for comments). Either way the compiler now guarantees CurrentPhase can never be anything except one of these three.
Step 2 — Store the phase in a var. var CurrentPhase : shell_hunt_phase = shell_hunt_phase.Lobby starts every round on the sand. Because it is a var, we reassign it with set.
Step 3 — Gate every transition. Each button handler checks the current phase first. A transition that is not drawn on the diagram (Lobby -> Celebration? no such arrow) simply does not exist in code, so illegal presses fall through to a friendly log line and nothing changes.
Step 4 — Route all changes through one doorway. EnterPhase is the only place set CurrentPhase appears. Announcing, logging — and later, starting timers or unlocking spawners — all hang off that one function.
Step 5 — Attach data with case. Verse enums do not carry fields, so the idiom is a helper function that case-switches on the value and returns the data: PhaseLabel returns each phase's banner text, PhaseBannerTime returns how long to show it. Add a phase later and the compiler walks you to every helper that needs a new arm.
Drop this device on your island, place two Button devices and a HUD Message device, and wire them up in the Details panel:
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# The three phases of the Shell Hunt round, in play order.
shell_hunt_phase := enum:
Lobby
Hunting
Celebration
# Runs the Shell Hunt round as a tiny state machine.
shell_hunt_phase_controller := class(creative_device):
# Wire to the driftwood "Start Hunt" button on the beach.
@editable
StartButton : button_device = button_device{}
# Wire to the "Turn In Shells" button at the judging table.
@editable
FinishButton : button_device = button_device{}
# Announces every phase change to all players.
@editable
Announcer : hud_message_device = hud_message_device{}
# The single source of truth for the whole round.
var CurrentPhase : shell_hunt_phase = shell_hunt_phase.Lobby
# Wraps a plain string so message-typed parameters accept it.
Msg<localizes>(S : string) : message = "{S}"
OnBegin<override>()<suspends> : void =
StartButton.InteractedWithEvent.Subscribe(OnStartPressed)
FinishButton.InteractedWithEvent.Subscribe(OnFinishPressed)
AnnouncePhase()
OnStartPressed(Interactor : agent) : void =
# This transition only exists FROM Lobby. Anywhere else: ignored.
if (CurrentPhase = shell_hunt_phase.Lobby):
EnterPhase(shell_hunt_phase.Hunting)
else:
Print("Start pressed during {PhaseLabel(CurrentPhase)} - ignored")
OnFinishPressed(Interactor : agent) : void =
# Only a live hunt can be finished.
if (CurrentPhase = shell_hunt_phase.Hunting):
EnterPhase(shell_hunt_phase.Celebration)
else:
Print("Finish pressed during {PhaseLabel(CurrentPhase)} - ignored")
# ONE doorway for every phase change: reassign, then react.
EnterPhase(NewPhase : shell_hunt_phase) : void =
set CurrentPhase = NewPhase
AnnouncePhase()
AnnouncePhase() : void =
Announcer.Show(Msg(PhaseLabel(CurrentPhase)), ?DisplayTime := PhaseBannerTime(CurrentPhase))
Print("Phase is now: {PhaseLabel(CurrentPhase)}")
# --- Data attached to each phase (the enum-with-data pattern) ---
# Each phase's on-screen banner text.
PhaseLabel(Phase : shell_hunt_phase) : string =
case (Phase):
shell_hunt_phase.Lobby => "Lobby - hunters gathering on the sand"
shell_hunt_phase.Hunting => "The shell hunt is ON!"
shell_hunt_phase.Celebration => "Hunt over - crown the shell champion!"
_ => "Unknown phase"
# How long each phase's banner stays on screen, in seconds.
PhaseBannerTime(Phase : shell_hunt_phase) : float =
case (Phase):
shell_hunt_phase.Lobby => 4.0
shell_hunt_phase.Hunting => 6.0
shell_hunt_phase.Celebration => 10.0
_ => 4.0
Playtest it: press Start Hunt on the beach and the banner flips to Hunting for six seconds. Press it again — ignored, with a log line telling you why. Press Turn In Shells and the Celebration banner lingers a full ten seconds, exactly as PhaseBannerTime promised.
Common patterns
Pattern 1 — Phase-gate every future handler
The whole point of the machine is that other systems read it. When the capstone adds shell pickups, the handler opens with the same guard — one if line makes an entire feature phase-aware:
# A future shell-pickup handler: gated by the SAME phase var.
OnShellCollected(Interactor : agent) : void =
if (CurrentPhase = shell_hunt_phase.Hunting):
Print("Shell counted!") # scorekeeping comes in a later lesson
else:
Print("Shell ignored - the hunt is not running")
Pattern 2 — A single cycle button
Prototyping solo and tired of running between two buttons? Wire one button to a handler that walks the loop with an if / else if chain (the same shape our case helpers use, just choosing actions instead of returning data):
# One button that walks the phases in a fixed loop.
OnCyclePressed(Interactor : agent) : void =
if (CurrentPhase = shell_hunt_phase.Lobby):
EnterPhase(shell_hunt_phase.Hunting)
else if (CurrentPhase = shell_hunt_phase.Hunting):
EnterPhase(shell_hunt_phase.Celebration)
else:
EnterPhase(shell_hunt_phase.Lobby) # back to the beach for round two
Both patterns keep the sacred rule intact: they call EnterPhase, never set CurrentPhase directly.
Where this goes next
This device is the first rung of the state-machine arc and the spine of the Shell Hunt capstone. Upcoming South Shores lessons import it directly:
- Shell pickups & scorekeeping subscribe their handlers and gate them on
CurrentPhase = shell_hunt_phase.Hunting. - The round timer calls
EnterPhase(shell_hunt_phase.Celebration)when time runs out — a second caller, still one doorway. - The capstone assembly wires every device on the beach through this one
var, so the whole minigame starts, runs, and ends in lockstep.
When you meet richer state machines later in the arc (entry/exit actions, async phases with Inkbeard in the west coves), they are this exact pattern wearing a bigger hat.