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 toSetActiveDurationif 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 lessonSources
/guides/constantsBiloxi Studios original lesson
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