Overview
The case expression in Verse is a multi-branch selector: you hand it one value and it tests that value against a list of possibilities, running the first branch that matches. When the value is an int, case is the idiomatic replacement for a long if / else if / else ladder.
When to reach for it:
- Switching game phases by round number (
1,2,3, …) - Mapping a score tier (
0–2= bronze,3–5= silver, …) to a reward - Routing a player to different spawn rooms based on a team index
- Driving an NPC state machine where states are encoded as integers
case is a pure Verse language feature — it has no device API surface of its own. It compiles to the same branching logic as if, but is far more readable for discrete integer values.
API Reference
(API surface could not be resolved for this device.)
Walkthrough
Scenario: A wave-survival island. At the start of each wave the device reads the current wave number (stored in a var) and configures the round differently: wave 1 is a tutorial, wave 2 doubles the enemy count, wave 3 triggers a boss encounter, and any later wave resets to a hard default.
The device also uses a button_device to let the host manually advance waves, and a cinematic_sequence_device to play the boss intro on wave 3.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
wave_manager_device := class(creative_device):
# Wire these up in the UEFN editor
@editable
AdvanceWaveButton : button_device = button_device{}
@editable
BossCinematic : cinematic_sequence_device = cinematic_sequence_device{}
# Mutable wave counter
var CurrentWave : int = 0
# Localized helper so we can pass strings as message
WaveMsg<localizes>(S : string) : message = "{S}"
OnBegin<override>()<suspends> : void =
# Subscribe to the button so pressing it advances the wave
AdvanceWaveButton.InteractedWithEvent.Subscribe(OnAdvanceWave)
ConfigureWave(CurrentWave)
# Called whenever the host presses the Advance Wave button
OnAdvanceWave(Agent : agent) : void =
set CurrentWave = CurrentWave + 1
ConfigureWave(CurrentWave)
# The heart of this device — case on the wave int
ConfigureWave(Wave : int) : void =
case (Wave):
0 =>
# Wave 0: tutorial — nothing dangerous yet
Print("Wave 0: Tutorial — no enemies spawned.")
1 =>
# Wave 1: light combat
Print("Wave 1: Light enemies incoming!")
2 =>
# Wave 2: ramp up
Print("Wave 2: Double enemy count!")
3 =>
# Wave 3: boss fight — play the cinematic
Print("Wave 3: BOSS INCOMING!")
BossCinematic.Play()
_ =>
# Any wave beyond 3: hard mode
Print("Wave {Wave}: Endless hard mode!")
Line-by-line explanation:
| Lines | What's happening |
|---|---|
@editable fields |
Declare the button and cinematic so you can wire them in the UEFN editor. |
var CurrentWave : int = 0 |
Mutable integer that tracks which wave we're on. |
WaveMsg<localizes> |
Helper that wraps a string into a message — required for any API that takes message. Not used with Print here, but shown as the correct pattern. |
OnBegin |
Subscribes the button's InteractedWithEvent and kicks off wave 0. |
OnAdvanceWave |
Increments the counter and re-runs ConfigureWave. |
case (Wave): |
Opens the case expression on the int variable. |
0 =>, 1 =>, 2 =>, 3 => |
Literal integer arms — each runs its block when Wave equals that value. |
BossCinematic.Play() |
A real device method called inside the case arm. |
_ => |
The mandatory default arm — catches every value not listed above. |
Important: Verse requires the default arm (
_ =>). The compiler will reject acaseexpression that doesn't cover all possibilities.
Common patterns
Pattern 1 — Score-tier reward routing
A player finishes a race. Their finish position (1st, 2nd, 3rd, or worse) determines which item granter fires. This shows case driving three different item_granter_device calls.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
race_reward_device := class(creative_device):
@editable
GoldGranter : item_granter_device = item_granter_device{}
@editable
SilverGranter : item_granter_device = item_granter_device{}
@editable
BronzeGranter : item_granter_device = item_granter_device{}
@editable
FinishTrigger : trigger_device = trigger_device{}
# Simulated finish position — in a real island this would come
# from a leaderboard or scoring device.
var FinishPosition : int = 1
OnBegin<override>()<suspends> : void =
FinishTrigger.TriggeredEvent.Subscribe(OnPlayerFinished)
OnPlayerFinished(Agent : ?agent) : void =
if (A := Agent?):
GrantRewardForPosition(FinishPosition, A)
GrantRewardForPosition(Position : int, Agent : agent) : void =
case (Position):
1 =>
GoldGranter.GrantItem(Agent)
2 =>
SilverGranter.GrantItem(Agent)
3 =>
BronzeGranter.GrantItem(Agent)
_ =>
# 4th place and beyond — no reward
Print("No reward for position {Position}.")
Key point: Each arm calls a different device method (GrantItem on three separate granters). The _ => default arm handles any position outside the podium.
Pattern 2 — Door routing by team index
Four teams each get their own vault door. A button_device press checks the pressing agent's team index (0–3) and enables only their door via barrier_device. This demonstrates case with side-effects on multiple devices.
using { /Fortnite.com/Devices }
using { /Fortnite.com/Game }
using { /Verse.org/Simulation }
team_vault_device := class(creative_device):
@editable
VaultButton : button_device = button_device{}
@editable
DoorTeam0 : barrier_device = barrier_device{}
@editable
DoorTeam1 : barrier_device = barrier_device{}
@editable
DoorTeam2 : barrier_device = barrier_device{}
@editable
DoorTeam3 : barrier_device = barrier_device{}
OnBegin<override>()<suspends> : void =
VaultButton.InteractedWithEvent.Subscribe(OnButtonPressed)
OnButtonPressed(Agent : agent) : void =
# GetTeamIndex returns an int — perfect for case
if (FortAgent := Agent.GetFortCharacter[]):
TeamIdx := FortAgent.GetTeamIndex()
OpenDoorForTeam(TeamIdx)
OpenDoorForTeam(TeamIndex : int) : void =
case (TeamIndex):
0 => DoorTeam0.Disable()
1 => DoorTeam1.Disable()
2 => DoorTeam2.Disable()
3 => DoorTeam3.Disable()
_ =>
# Spectators or unassigned players — do nothing
Print("Unknown team index: {TeamIndex}")
Key point: case arms can be single-expression (no block braces needed). The default arm is still required even when you intend to do nothing — use a Print or a no-op comment block.
Pattern 3 — NPC alert state machine
An NPC cycles through states encoded as integers: 0 = idle, 1 = patrol, 2 = alert, 3 = combat. A trigger_device bumps the state each time a player enters the zone. This shows case inside a loop, updating a var.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
npc_state_machine_device := class(creative_device):
@editable
ZoneTrigger : trigger_device = trigger_device{}
@editable
AlertCinematic : cinematic_sequence_device = cinematic_sequence_device{}
# 0=idle, 1=patrol, 2=alert, 3=combat
var NPCState : int = 0
OnBegin<override>()<suspends> : void =
ZoneTrigger.TriggeredEvent.Subscribe(OnZoneEntered)
OnZoneEntered(Agent : ?agent) : void =
# Advance state, wrapping back to 0 after combat
NextState := if (NPCState < 3) then NPCState + 1 else 0
set NPCState = NextState
ApplyState(NPCState)
ApplyState(State : int) : void =
case (State):
0 =>
Print("NPC is now IDLE.")
1 =>
Print("NPC is now PATROLLING.")
2 =>
Print("NPC is now ALERT!")
AlertCinematic.Play()
3 =>
Print("NPC is now in COMBAT!")
_ =>
Print("Unknown NPC state: {State}")
Key point: case works perfectly inside helper methods called from event handlers. Separating the state-transition logic (OnZoneEntered) from the state-application logic (ApplyState) keeps each function focused.
Gotchas
1. The default arm (_ =>) is mandatory
Verse's case expression must be exhaustive. If you omit _ =>, the compiler will error. Even if you're certain your integer will only ever be 0, 1, or 2, you still need the default arm. Use it to log unexpected values so you catch bugs early.
2. Arms match exact integer literals only
case on int matches exact literal values — you cannot write a range like 1..5 => or a condition like > 3 =>. For range matching, use if / else if or clamp the value before passing it to case.
# WRONG — ranges are not valid case arms
# case (Score):
# 0..49 => ...
# RIGHT — clamp to a tier first, then case on the tier
Tier : int = if (Score < 50) then 0 else if (Score < 80) then 1 else 2
case (Tier):
0 => Print("Bronze")
1 => Print("Silver")
2 => Print("Gold")
_ => Print("Unknown tier")
3. No fall-through between arms
Unlike C or C++, Verse case arms do not fall through. Each arm is completely independent. If you want two integer values to share the same code, you must either duplicate the code or call a shared helper function from both arms.
4. case is an expression, not just a statement
case returns a value — you can assign its result to a variable. This is powerful but means every arm must produce a value of the same type if you're using it as an expression. If you're using it purely for side effects (calling device methods), this doesn't matter.
5. int ≠ float — no auto-conversion
Verse never silently converts between int and float. If your source value is a float (e.g., from a timer or physics calculation), you must explicitly convert it with Int[FloatValue] (a failable conversion) before passing it to case.
# FloatVal : float = 2.0
# case (FloatVal): -- COMPILE ERROR, case expects int here
# Correct:
if (IntVal := Int[FloatVal]):
case (IntVal):
2 => Print("Got two!")
_ => Print("Other")
6. Localized text vs raw strings
If any device method in a case arm takes a message parameter (e.g., HUD text), you cannot pass a raw string literal. Declare a <localizes> helper:
MyMsg<localizes>(S : string) : message = "{S}"
// Then pass: MyMsg("Round 3 — Boss Fight!")
There is no StringToMessage function in Verse.