Crow's Nest Constants: immutable bindings

In this lesson you'll learn to

  • βœ“Student can declare constants and immutable data bindings and explain why config values should never be mutable.

Student can declare constants and immutable data bindings and explain why config values should never be mutable.

πŸ“– Reference & full walkthrough: Crow's Nest Constants: immutable bindings

🧩 Your capstone piece: RaceConfig constants: LapCount, StormTimeoutSeconds, MinRacers, CheckpointCount


Overview

A constant in Verse is a named storage location whose value is fixed for the life of the program β€” once you write TreasureTime : float = 90.0, that binding never changes. Constants differ from variables (declared with var) in one key way: you cannot assign a new value to them after initialization.

Why does this matter on your island? Because magic numbers scattered through your Verse files are a maintenance nightmare. When your pirate cove race needs a 90-second countdown, a 10-second urgency warning, and a 5-second grace period, naming those values as constants means you change one line instead of hunting through dozens of method calls.

Constants also communicate intent. SetActiveDuration(UrgencyThreshold, Agent) tells every future reader exactly what that number means; SetActiveDuration(10.0, Agent) tells them nothing.

Reach for constants when:

  • A number or value is used in more than one place.
  • A value has a real-world meaning (a duration, a score threshold, a lap count).
  • You want the compiler to catch typos β€” a misspelled constant name is a compile error; a mistyped literal is a silent bug.

API Reference

timer_device

Provides a way to keep track of the time something has taken, either for scoreboard purposes, or to trigger actions. It can be configured in several ways, either acting as a countdown to an event that is triggered at the end, or as a stopwatch for an action that needs to be completed before a set time runs out.

Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse).

timer_device<public> := class<concrete><final>(creative_device_base):

Events (subscribe a handler to react):

Event Signature Description
SuccessEvent SuccessEvent<public>:listenable(?agent) Signaled when the timer completes or ends with success. Sends the agent that activated the timer, if any.
FailureEvent FailureEvent<public>:listenable(?agent) Signaled when the timer completes or ends with failure. Sends the agent that activated the timer, if any.
StartUrgencyModeEvent StartUrgencyModeEvent<public>:listenable(?agent) Signaled when the timer enters Urgency Mode. Sends the agent that activated the timer, if any.

Methods (call these to make the device act):

Method Signature Description
Enable Enable<public>(Agent:agent):void Enables this device for Agent.
Enable Enable<public>():void Enables this device.
Disable Disable<public>(Agent:agent):void Disables this device for Agent. While disabled this device will not receive signals.
Disable Disable<public>():void Disables this device. While disabled this device will not receive signals.
ResetForAll ResetForAll<public>(Agent:agent):void Resets the timer back to its base time and stops it for all agents.
ResetForAll ResetForAll<public>():void Resets the timer back to its base time and stops it for all agents.
Start Start<public>(Agent:agent):void Starts the timer for Agent.
Start Start<public>():void Starts the timer.
Pause Pause<public>(Agent:agent):void Pauses the timer for Agent.
Pause Pause<public>():void Pauses the timer.
Resume Resume<public>(Agent:agent):void Resumes the timer for Agent.
Resume Resume<public>():void Resumes the timer.
Complete Complete<public>(Agent:agent):void Completes the timer for Agent.
Complete Complete<public>():void Completes the timer.
StartForAll StartForAll<public>(Agent:agent):void Starts the timer for all agents.
StartForAll StartForAll<public>():void Starts the timer for all agents.
PauseForAll PauseForAll<public>(Agent:agent):void Pauses the timer for all agents.
PauseForAll PauseForAll<public>():void Pauses the timer for all agents.
ResumeForAll ResumeForAll<public>(Agent:agent):void Resumes the timer for all agents.
ResumeForAll ResumeForAll<public>():void Resumes the timer for all agents.
CompleteForAll CompleteForAll<public>(Agent:agent):void Completes the timer for all agents.
CompleteForAll CompleteForAll<public>():void Completes the timer for all agents.
Save Save<public>(Agent:agent):void Saves this device's data for Agent.
Load Load<public>(Agent:agent):void Loads this device's saved data for Agent.
ClearPersistenceData ClearPersistenceData<public>(Agent:agent):void Clears this device's saved data for Agent.
ClearPersistenceDataForAll ClearPersistenceDataForAll<public>(Agent:agent):void Clears this device's saved data for all agents.
ClearPersistenceDataForAll ClearPersistenceDataForAll<public>():void Clears this device's saved data for all agents.
SetActiveDuration SetActiveDuration<public>(Time:float, Agent:agent):void Sets the remaining time (in seconds) on the timer, if active, on Agent.
SetActiveDuration SetActiveDuration<public>(Time:float):void Sets the remaining time (in seconds) on the timer, if active. Use this function if the timer is set to use the same time for all agent's.
GetActiveDuration GetActiveDuration<public>(Agent:agent)<transacts>:float Returns the remaining time (in seconds) on the timer for Agent.
GetActiveDuration GetActiveDuration<public>()<transacts>:float Returns the remaining time (in seconds) on the timer if it is set to be global.
SetLapTime SetLapTime<public>(Agent:agent):void Sets the lap time indicator for Agent.
SetLapTimeForAll SetLapTimeForAll<public>(Agent:agent):void Sets the lap time indicator for all agents.
SetLapTimeForAll SetLapTimeForAll<public>():void Sets the lap time indicator for all agents.
SetMaxDuration SetMaxDuration<public>(Time:float):void Sets the maximum duration of the timer (in seconds).
GetMaxDuration GetMaxDuration<public>()<transacts>:float Returns the maximum duration of the timer (in seconds).
IsStatePerAgent IsStatePerAgent<public>()<transacts><decides>:void Succeeds if this device is tracking timer state for each individual agent independently. Fails if state is being tracked globally for all agent's.

Walkthrough

The scene: a 2D cel-shaded pirate cove at golden hour. A galleon sits at anchor in the lagoon. Players spawn on the dock and must reach the treasure chest at the clifftop before the countdown hits zero. If they make it, SuccessEvent fires and the chest bursts open. If time runs out, FailureEvent fires and the plank drops them into the sea.

We use four constants to drive the whole experience:

  • TotalRaceTime β€” the full countdown duration.
  • UrgencyThreshold β€” when urgency mode kicks in (the timer turns red and pulses).
  • GracePeriodTime β€” extra seconds granted to a player who reaches a mid-dock checkpoint.
  • MaxRaceDuration β€” a safety cap passed to SetActiveDuration if a power-up is collected.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

# ─── Pirate Cove Treasure Race ───────────────────────────────────────────────
# Constants drive every timing decision; no magic numbers anywhere.

pirate_cove_race := class(creative_device):

    # ── Editable device references (wire these up in UEFN) ──────────────────
    @editable
    RaceTimer : timer_device = timer_device{}

    # ── Module-level constants ───────────────────────────────────────────────
    # Total seconds a player has to reach the treasure chest.
    TotalRaceTime : float = 90.0

    # When the timer drops to this many seconds, urgency mode fires.
    # (Configured on the timer_device itself; we also use it in code.)
    UrgencyThreshold : float = 15.0

    # Bonus seconds added when a player hits the mid-dock checkpoint.
    GracePeriodTime : float = 10.0

    # Hard cap used when a speed-boost power-up resets the active duration.
    MaxRaceDuration : float = 120.0

    # ── Lifecycle ────────────────────────────────────────────────────────────
    OnBegin<override>()<suspends> : void =
        # Subscribe to all three timer events before starting anything.
        RaceTimer.SuccessEvent.Subscribe(OnTreasureReached)
        RaceTimer.FailureEvent.Subscribe(OnTimeExpired)
        RaceTimer.StartUrgencyModeEvent.Subscribe(OnUrgencyStarted)

        # Kick off the race for every player on the island.
        RaceTimer.StartForAll()

    # ── Event handlers ───────────────────────────────────────────────────────

    # Called when a player reaches the chest in time.
    OnTreasureReached(MaybeAgent : ?agent) : void =
        if (A := MaybeAgent?):
            # Mark this individual player's run as complete.
            RaceTimer.Complete(A)

    # Called when the countdown hits zero β€” the plank drops!
    OnTimeExpired(MaybeAgent : ?agent) : void =
        # No agent means the global timer expired for everyone.
        # Reset so the next round starts clean.
        RaceTimer.ResetForAll()

    # Called when the timer enters urgency mode (the red-pulse phase).
    OnUrgencyStarted(MaybeAgent : ?agent) : void =
        # Nothing extra to do here in this example,
        # but this is where you'd trigger a siren SFX device.
        # The constant UrgencyThreshold documents WHY this fires.
        if (A := MaybeAgent?):
            # Pause briefly so the player sees the urgency flash, then resume.
            RaceTimer.Pause(A)
            RaceTimer.Resume(A)

    # ── Called externally when a mid-dock checkpoint is hit ──────────────────
    # Wire a trigger_device's TriggeredEvent to call this from another device,
    # or call it directly from a companion Verse class.
    GrantGracePeriod(Agent : agent) : void =
        # Add GracePeriodTime to whatever is left on the clock.
        # GetActiveDuration returns the remaining seconds for this agent.
        CurrentTime : float = RaceTimer.GetActiveDuration(Agent)
        NewTime : float = CurrentTime + GracePeriodTime
        # Cap at MaxRaceDuration so no one gets infinite time.
        FinalTime : float = if (NewTime > MaxRaceDuration) then MaxRaceDuration else NewTime
        RaceTimer.SetActiveDuration(FinalTime, Agent)

Line-by-line explanation

Lines What's happening
TotalRaceTime : float = 90.0 Explicit-type constant β€” identifier, colon, type, equals, value. The type annotation is required at class scope.
UrgencyThreshold : float = 15.0 Same pattern. This value matches what you set in the timer_device's Urgency Mode Threshold property in UEFN β€” keeping code and editor in sync.
GracePeriodTime : float = 10.0 Used only in GrantGracePeriod, but naming it here means changing the grace period is a one-line edit.
MaxRaceDuration : float = 120.0 Passed to SetActiveDuration as a safety cap β€” a named constant makes the intent obvious.
RaceTimer.StartForAll() Starts the countdown for every player simultaneously β€” the lagoon race begins!
RaceTimer.SuccessEvent.Subscribe(OnTreasureReached) Hooks the success path; fires when the timer is completed for an agent.
RaceTimer.FailureEvent.Subscribe(OnTimeExpired) Hooks the failure path; fires when the countdown reaches zero.
RaceTimer.StartUrgencyModeEvent.Subscribe(OnUrgencyStarted) Hooks the urgency-mode transition β€” the moment the timer turns red.
RaceTimer.GetActiveDuration(Agent) Reads the remaining time for this specific agent.
RaceTimer.SetActiveDuration(FinalTime, Agent) Writes a new remaining time β€” used here to add the grace-period bonus.
if (A := MaybeAgent?) Standard option-unwrap pattern; SuccessEvent sends ?agent, not agent.

Common patterns

Pattern 1 β€” Inferred-type constants inside a function

Inside a function body you can drop the explicit type and let Verse infer it. Use := instead of : type =.

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

cove_lap_timer := class(creative_device):

    @editable
    LapTimer : timer_device = timer_device{}

    OnBegin<override>()<suspends> : void =
        # Inferred-type constants β€” no explicit type annotation needed here.
        LapSeconds := 45.0          # float, inferred
        LapCount   := 3             # int,   inferred
        LapLabel   := "Cove Sprint" # string, inferred (not passed to a message param)

        # Use the float constant to set the lap time for all players.
        LapTimer.SetLapTimeForAll(LapSeconds)
        LapTimer.StartForAll()

Key point: := is shorthand for : <inferred_type> =. It is only valid inside a function β€” at class scope you must write the full : type = form.


Pattern 2 β€” Pausing and resuming with per-agent constants

This pattern shows how constants make per-agent timer manipulation readable. A player who sails into the fog bank gets their timer paused; sailing back out resumes it.

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

fog_bank_timer := class(creative_device):

    @editable
    SailTimer : timer_device = timer_device{}

    # How long the fog penalty lasts before auto-resume (seconds).
    FogPenaltyDuration : float = 8.0

    OnBegin<override>()<suspends> : void =
        SailTimer.SuccessEvent.Subscribe(OnSailComplete)
        SailTimer.FailureEvent.Subscribe(OnSailFailed)
        SailTimer.Enable()
        SailTimer.StartForAll()

    # A companion trigger calls this when a player enters the fog bank.
    EnterFog(Agent : agent) : void =
        SailTimer.Pause(Agent)

    # A companion trigger calls this when a player exits the fog bank.
    ExitFog(Agent : agent) : void =
        # Deduct the penalty by reducing remaining time.
        Remaining : float = SailTimer.GetActiveDuration(Agent)
        Penalized : float = Remaining - FogPenaltyDuration
        SafeTime  : float = if (Penalized > 0.0) then Penalized else 0.1
        SailTimer.SetActiveDuration(SafeTime, Agent)
        SailTimer.Resume(Agent)

    OnSailComplete(MaybeAgent : ?agent) : void =
        if (A := MaybeAgent?):
            SailTimer.Save(A)

    OnSailFailed(MaybeAgent : ?agent) : void =
        SailTimer.ResetForAll()

Pattern 3 β€” Disabling and re-enabling the timer between rounds

Between rounds on the pirate cove island, the timer should be inert. Constants define the round structure; Disable / Enable / ResetForAll manage state.

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

round_manager := class(creative_device):

    @editable
    RoundTimer : timer_device = timer_device{}

    # Number of rounds before the island session ends.
    TotalRounds : int = 5

    # Seconds between rounds (lobby / intermission).
    IntermissionSeconds : float = 20.0

    OnBegin<override>()<suspends> : void =
        RoundTimer.SuccessEvent.Subscribe(OnRoundWon)
        RoundTimer.FailureEvent.Subscribe(OnRoundLost)

        RoundsPlayed := 0  # inferred int constant β€” reset each iteration below
        RunRounds(TotalRounds)

    RunRounds(Rounds : int)<suspends> : void =
        Index := 0
        loop:
            if (Index >= Rounds):
                break
            # Arm the timer for the new round.
            RoundTimer.Enable()
            RoundTimer.StartForAll()
            # Suspend until the round ends (handled by event callbacks).
            Sleep(IntermissionSeconds)
            # Tear down between rounds.
            RoundTimer.Disable()
            RoundTimer.ResetForAll()

    OnRoundWon(MaybeAgent : ?agent) : void =
        if (A := MaybeAgent?):
            RoundTimer.Complete(A)

    OnRoundLost(MaybeAgent : ?agent) : void =
        RoundTimer.ResetForAll()

Gotchas

1. Class-scope constants require an explicit type

At class scope, := is not valid. You must write MyValue : float = 3.14. Using := at class scope is a compile error.

# βœ… Correct β€” class scope
MyDevice := class(creative_device):
    RaceTime : float = 90.0   # explicit type required

# ❌ Wrong β€” class scope
MyDevice2 := class(creative_device):
    RaceTime := 90.0          # compile error at class scope

2. Constants are NOT variables β€” you cannot reassign them

Once bound, a constant's value is fixed. If you need to track changing state (e.g. current lap number), declare a var field instead.

# βœ… Constant β€” never changes
MaxLaps : int = 3

# βœ… Variable β€” changes during play
var CurrentLap : int = 0

3. No implicit int ↔ float conversion

Verse will not silently convert between int and float. If SetActiveDuration expects a float and you pass an int constant, you get a type error. Always match the type to the API signature.

# ❌ Wrong β€” int passed where float is required
BadTime : int = 90
RaceTimer.SetActiveDuration(BadTime, Agent)  # type error

# βœ… Correct
GoodTime : float = 90.0
RaceTimer.SetActiveDuration(GoodTime, Agent)

4. Option unwrap on timer events

SuccessEvent, FailureEvent, and StartUrgencyModeEvent all send ?agent (an optional agent). Always unwrap with if (A := MaybeAgent?): before using the agent. Skipping the unwrap is a compile error.

5. message vs string for UI text

If you ever pass a constant string to a device parameter typed message, you cannot pass a raw string literal. Declare a localizer function:

TimerLabel<localizes>(S : string) : message = "{S}"
# Then use: TimerLabel("Cove Sprint")

There is no StringToMessage function in Verse.

6. GetActiveDuration is per-agent

GetActiveDuration(Agent) returns the remaining time for that specific agent. If you want a global remaining time, make sure your timer is configured as a non-per-agent timer in the device settings (IsStatePerAgent returns whether the device tracks state individually).

πŸ”’

Keep going β€” free

You've read the intro. The rest of this lesson is free for members. Sign in to continue and track your progress.

Sign in free to continue this lesson
Pass the quiz above to chart this quest in your Journal.

Sources

/guides/constants
Source

Biloxi Studios original lesson

Guided course

Add this lesson to your free study plan.

🧭 The Keeper’s log

Quest complete? Chart your next heading from the πŸ¦‘ West Coves expedition.

β›΅ Take me there