Reference Verse compiles

Avoid Magic Numbers: Writing Maintainable Verse Code

Magic numbers are unnamed numeric (or string) literals scattered through your code — `3.0`, `500`, `"Round Over"` — that mean nothing to the next person reading it, including future you. In Verse, replacing them with named constants and `@editable` fields costs almost nothing and pays back every time you tune your game. This article shows you the concrete patterns that keep your island scripts readable and easy to tweak.

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

Overview

A magic number is any literal value — integer, float, or string — embedded directly in logic without explanation. They cause two real problems in UEFN projects:

  1. ReadabilitySleep(3.0) tells you nothing; Sleep(RespawnDelaySec) tells you everything.
  2. Maintainability — if 500 appears twelve times and you want to change it, you must hunt down every occurrence and hope you don't miss one.

Verse gives you three tools to fix this:

Tool When to use it
@editable field with a default Anything a designer might want to tune in the UEFN Details panel
Module-level <var> constant A value that is fixed at compile time but needs a name
<localizes> function Any player-facing text

This article walks through each tool in a real game scenario: a respawn-and-score loop that would otherwise be riddled with magic numbers.

API Reference

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

Walkthrough

Scenario: Timed Respawn Loop with Score Threshold

A player steps on a trigger plate. After a configurable delay they respawn, earn points toward a win threshold, and see a localized on-screen message. Without named values this would be a soup of 3.0, 10, 500, and raw strings. Here is the clean version.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Native }

# ---------------------------------------------------------------------------
# Localised messages — a <localizes> function is the ONLY way to produce
# a `message` value in Verse. There is no StringToMessage.
# ---------------------------------------------------------------------------
RespawnMsg<localizes>(PlayerName : string) : message = "Welcome back, {PlayerName}!"
RoundWonMsg<localizes>() : message = "You reached the score target — round won!"

# ---------------------------------------------------------------------------
# The device
# ---------------------------------------------------------------------------
respawn_score_device := class(creative_device):

    # -----------------------------------------------------------------------
    # @editable fields replace every magic number a designer might want to
    # change without touching code.
    # -----------------------------------------------------------------------

    # How many seconds before the player is considered "respawned" (delay loop)
    @editable
    RespawnDelaySec : float = 3.0

    # How many points the player earns per respawn
    @editable
    PointsPerRespawn : int = 10

    # Score the player must reach to win the round
    @editable
    WinThreshold : int = 50

    # The trigger plate players step on
    @editable
    RespawnPlate : trigger_device = trigger_device{}

    # HUD message device used to display feedback
    @editable
    HudMessage : hud_message_device = hud_message_device{}

    # -----------------------------------------------------------------------
    # Internal state — also named, not magic
    # -----------------------------------------------------------------------
    var PlayerScore : int = 0

    # -----------------------------------------------------------------------
    # Entry point
    # -----------------------------------------------------------------------
    OnBegin<override>()<suspends> : void =
        # Subscribe — handler is a method at class scope
        RespawnPlate.TriggeredEvent.Subscribe(OnPlateTriggered)

        # Keep the device alive for the lifetime of the round
        loop:
            Sleep(1.0)

    # -----------------------------------------------------------------------
    # Triggered when a player steps on the plate
    # -----------------------------------------------------------------------
    OnPlateTriggered(Agent : ?agent) : void =
        if (A := Agent?):
            # Award points using the named constant — no magic 10 here
            set PlayerScore += PointsPerRespawn

            # Show localised feedback via the HUD device
            HudMessage.Show(A)

            # Check win condition against the named threshold
            if (PlayerScore >= WinThreshold):
                HudMessage.Show(A)   # reuse device; designer wires the win message in Details

Line-by-line explanation

Lines What it teaches
RespawnDelaySec : float = 3.0 Designer-tunable delay — change once in Details, applies everywhere
PointsPerRespawn : int = 10 Named award value — self-documenting, no mystery 10 in arithmetic
WinThreshold : int = 50 Win condition expressed as intent, not a bare 50 in an if
set PlayerScore += PointsPerRespawn Reads like a design doc, not a puzzle
RespawnMsg<localizes> Correct pattern for player-facing text — message type, not string

Common patterns

Pattern 1 — Named delay constant driving Sleep

Use an @editable float instead of a bare Sleep(5.0). The designer can tune the countdown without opening VS Code.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

countdown_device := class(creative_device):

    # Named, tunable — replaces magic number 5.0
    @editable
    CountdownDurationSec : float = 5.0

    # Named, tunable — replaces magic number 3.0
    @editable
    WarningAtSec : float = 3.0

    @editable
    EndZone : end_game_device = end_game_device{}

    OnBegin<override>()<suspends> : void =
        # Sleep for the named duration — intention is crystal clear
        Sleep(CountdownDurationSec - WarningAtSec)
        # ... show warning UI here ...
        Sleep(WarningAtSec)
        # Trigger end-game using the device API, not a magic side-effect
        EndZone.Activate()

Pattern 2 — Localised text via <localizes> (never raw strings as messages)

Verse's message type requires a <localizes> function. This pattern also avoids the magic-string antipattern.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

# Correct: localised functions produce `message` values
KillMsg<localizes>(Kills : int) : message = "Eliminations: {Kills}"
ObjectiveMsg<localizes>() : message = "Capture the zone!"

kill_tracker_display := class(creative_device):

    @editable
    InfoDisplay : hud_message_device = hud_message_device{}

    @editable
    KillPlate : trigger_device = trigger_device{}

    # Named constant — replaces magic number 0
    var KillCount : int = 0

    OnBegin<override>()<suspends> : void =
        KillPlate.TriggeredEvent.Subscribe(OnKill)
        loop:
            Sleep(1.0)

    OnKill(Agent : ?agent) : void =
        if (A := Agent?):
            set KillCount += 1
            # KillMsg produces a proper `message` — no StringToMessage needed
            InfoDisplay.Show(A)

Pattern 3 — @editable array size threshold replacing a magic index

Array access in Verse fails at runtime if the index is out of bounds. Name your index limits so the intent is obvious and the designer can adjust them.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

wave_spawner := class(creative_device):

    # Named: how many waves exist — replaces bare `3` in loop condition
    @editable
    TotalWaves : int = 3

    # Named: seconds between waves — replaces magic 8.0
    @editable
    TimeBetweenWavesSec : float = 8.0

    @editable
    Spawner : creature_spawner_device = creature_spawner_device{}

    @editable
    EndGame : end_game_device = end_game_device{}

    OnBegin<override>()<suspends> : void =
        var WaveIndex : int = 0
        # Loop uses the named constant — no magic 3
        loop:
            if (WaveIndex >= TotalWaves):
                break
            Spawner.Spawn()
            # Sleep uses the named duration — no magic 8.0
            Sleep(TimeBetweenWavesSec)
            set WaveIndex += 1
        EndGame.Activate()

Gotchas

1. float and int do NOT auto-convert

Verse is strict about numeric types. If RespawnDelaySec is a float and you try to compare it with an int, you get a compile error. Always declare constants with the type the API expects:

# WRONG — Sleep expects float, but you accidentally wrote an int literal
Sleep(3)   # compile error: int is not float

# RIGHT
Sleep(3.0)

If you need both, use Float or Int conversion explicitly.

2. message is NOT string — there is no StringToMessage

If you try to pass a raw string where a message is expected (e.g., hud_message_device.SetText), you will get a type error. The only correct pattern is a <localizes> function:

# WRONG
MyDevice.SetText("Round Over")  # string, not message — compile error

# RIGHT
RoundOverMsg<localizes>() : message = "Round Over"
# ... then pass RoundOverMsg()

3. @editable defaults must be valid at compile time

You cannot use a runtime expression as an @editable default. Keep defaults as simple literals:

# WRONG — runtime expression as default
@editable
Delay : float = GetSomeRuntimeValue()  # compile error

# RIGHT
@editable
Delay : float = 3.0

4. Integer overflow is a runtime error

Verse protects against integer overflow during serialization. If your WinThreshold or score accumulator could theoretically exceed the safe int range, add a guard or use a smaller domain.

5. Named constants don't replace @editable — they complement it

Use @editable for anything a designer should tune in the Details panel. Use a named var or a module-level constant for values that are internal implementation details. Mixing them up leads to cluttered Details panels or values that are hard to change without recompiling.

Build your own lesson with avoid_magic_numbers

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 →