Reference Verse compiles

case on int: Branching Game Logic on the Cove

A pirate steps off the dock and rings the ship's bell — but how many times? On the sunny cove, a single integer decides everything: one ring shows a greeting, two rings arms a trap, three fires the cannon. The `case` expression on an int is how you fan one number out into many outcomes without a tangle of if/else.

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

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 (02 = bronze, 35 = 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 a case expression 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. intfloat — 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.

Guides & scripts that use player

Step-by-step tutorials that put this object to work.

Build your own lesson with player

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 →