Overview
Imagine a sun-drenched pirate cove: golden sand, turquoise water, and a rickety dock stretching out over the lagoon. The wooden planks of that dock have been there for a hundred years — immutable, unchanging. But the tide? The tide rises and falls. That's the difference between a constant and a variable in Verse.
Immutability is the default. When you write:
DockLength : int = 12
DockLength is bound to 12 forever. The compiler will refuse to let you change it later. This is not a limitation — it's a guarantee. It means any function that receives DockLength can trust the value won't shift under its feet.
When you do need a value that changes — say, the number of treasure chests a player has opened — you opt in to mutability with the var keyword:
var ChestsOpened : int = 0
Now ChestsOpened can be reassigned with set:
set ChestsOpened = ChestsOpened + 1
When to reach for immutability vs. mutability
| Situation | Use |
|---|---|
| A fixed game constant (max players, dock length, spawn position) | Immutable binding (no var) |
| A value that changes at runtime (score, health, trigger count) | var field |
| A computed result you pass around but never reassign | Immutable local binding |
The trigger_device is a perfect teaching partner here: it has a fixed identity (you place it once, it's always that trigger), but its state — how many times it can still fire — is mutable and managed internally by the device.
API Reference
vector3
3-dimensional vector with
floatcomponents.
Full public surface, resolved verbatim from the live Epic digest (Verse.digest.verse).
vector3<native><public> := struct<concrete><computes><persistable><uht_comparable>:
trigger_device
Used to relay events to other linked devices.
Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse). Inherited members are merged from trigger_base_device.
trigger_device<public> := class<concrete><final>(trigger_base_device):
Events (subscribe a handler to react):
| Event | Signature | Description |
|---|---|---|
TriggeredEvent |
TriggeredEvent<public>:listenable(?agent) |
Signaled when an agent triggers this device. Sends the agent that used this device. Returns false if no agent triggered the action (ex: it was triggered through code). |
Methods (call these to make the device act):
| Method | Signature | Description |
|---|---|---|
Trigger |
Trigger<public>(Agent:agent):void |
Triggers this device with Agent being passed as the agent that triggered the action. Use an agent reference when this device is setup to require one (for instance, you want to trigger the device only with a particular agent. |
Trigger |
Trigger<public>():void |
Triggers this device, causing it to activate its TriggeredEvent event. |
Enable |
Enable<public>():void |
Enables this device. |
Disable |
Disable<public>():void |
Disables this device. |
SetMaxTriggerCount |
SetMaxTriggerCount<public>(MaxCount:int):void |
Sets the maximum amount of times this device can trigger. * 0 can be used to indicate no limit on trigger count. * MaxCount is clamped between [0,20]. |
GetMaxTriggerCount |
GetMaxTriggerCount<public>()<transacts>:int |
Gets the maximum amount of times this device can trigger. * 0 indicates no limit on trigger count. |
GetTriggerCountRemaining |
GetTriggerCountRemaining<public>()<transacts>:int |
Returns the number of times that this device can still be triggered before hitting GetMaxTriggerCount. Returns 0 if GetMaxTriggerCount is unlimited. |
SetResetDelay |
SetResetDelay<public>(Time:float):void |
Sets the time (in seconds) after triggering, before the device can be triggered again (if MaxTrigger count allows). |
GetResetDelay |
GetResetDelay<public>()<transacts>:float |
Gets the time (in seconds) before the device can be triggered again (if MaxTrigger count allows). |
SetTransmitDelay |
SetTransmitDelay<public>(Time:float):void |
Sets the time (in seconds) which must pass after triggering, before this device informs other external devices that it has been triggered. |
GetTransmitDelay |
GetTransmitDelay<public>()<transacts>:float |
Gets the time (in seconds) which must pass after triggering, before this device informs other external devices that it has been triggered. |
Walkthrough
Scenario: A player steps onto a pressure plate on the pirate dock. A hidden trigger fires a cannon-blast effect (via trigger_device). The cannon can only fire three times before it runs dry. After each shot, we read back how many shots remain and store the count in a var field. When shots run out, the trigger is disabled so it can never fire again.
This example demonstrates:
- Immutable bindings for fixed game constants
- A
varfield for mutable runtime state trigger_devicemethods:SetMaxTriggerCount,GetTriggerCountRemaining,Trigger,Disabletrigger_deviceevent:TriggeredEvent
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# CannonController — placed on the pirate dock.
# Wire DockTrigger to a Trigger Device on the dock pressure plate.
# Wire CannonTrigger to a Trigger Device linked to your cannon VFX.
cannon_controller := class(creative_device):
# The pressure plate trigger on the dock — players walk over this.
@editable DockTrigger : trigger_device = trigger_device{}
# The cannon-blast trigger — fires the VFX/SFX linked in the editor.
@editable CannonTrigger : trigger_device = trigger_device{}
# IMMUTABLE: the cannon has exactly 3 shots. This never changes.
MaxShots : int = 3
# MUTABLE: tracks shots remaining at runtime.
var ShotsRemaining : int = 0
OnBegin<override>()<suspends> : void =
# Apply the immutable constant to the device at startup.
CannonTrigger.SetMaxTriggerCount(MaxShots)
# Read the device's own count back — stored in our mutable var.
set ShotsRemaining = CannonTrigger.GetTriggerCountRemaining()
# Subscribe to the dock plate. When a player steps on it,
# OnDockTriggered is called.
DockTrigger.TriggeredEvent.Subscribe(OnDockTriggered)
# Handler signature matches listenable(?agent).
OnDockTriggered(MaybeAgent : ?agent) : void =
# Unwrap the optional agent — the cannon fires regardless,
# but we only count a shot if a real player triggered it.
if (A := MaybeAgent?):
# Fire the cannon trigger with the agent reference.
CannonTrigger.Trigger(A)
else:
# Fired by code or a non-agent source — fire anonymously.
CannonTrigger.Trigger()
# Update our mutable shot counter from the device's live state.
set ShotsRemaining = CannonTrigger.GetTriggerCountRemaining()
# When shots are exhausted, disable the dock plate entirely.
if (ShotsRemaining <= 0):
DockTrigger.Disable()
Line-by-line explanation
| Lines | What's happening |
|---|---|
MaxShots : int = 3 |
Immutable binding — the compiler will reject any attempt to reassign this. |
var ShotsRemaining : int = 0 |
Mutable field — we'll update it with set each time the cannon fires. |
CannonTrigger.SetMaxTriggerCount(MaxShots) |
Pushes our immutable constant into the device's mutable internal counter. |
CannonTrigger.GetTriggerCountRemaining() |
Reads back the live remaining count — returns 0 when MaxTriggerCount is unlimited (it isn't here). |
DockTrigger.TriggeredEvent.Subscribe(OnDockTriggered) |
Registers our handler for the dock plate event. |
if (A := MaybeAgent?) |
Safely unwraps the ?agent — required because TriggeredEvent sends ?agent. |
CannonTrigger.Trigger(A) / CannonTrigger.Trigger() |
Fires the cannon with or without an agent reference. |
DockTrigger.Disable() |
Locks the dock plate once the cannon is spent — immutable logic, mutable device state. |
Common patterns
Pattern 1 — Read timing constants immutably, then apply them
Fixed reset and transmit delays are natural immutable constants. Declare them once, apply them at startup, and the device enforces them.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# TideBellController — a bell on the cove shore that rings on a fixed rhythm.
tide_bell_controller := class(creative_device):
@editable BellTrigger : trigger_device = trigger_device{}
# Immutable timing constants — set once, never reassigned.
RingCooldown : float = 5.0 # seconds before the bell can ring again
BroadcastDelay : float = 1.5 # seconds before linked devices hear the ring
OnBegin<override>()<suspends> : void =
# Apply immutable constants to the device's mutable timing state.
BellTrigger.SetResetDelay(RingCooldown)
BellTrigger.SetTransmitDelay(BroadcastDelay)
# Confirm the values were applied by reading them back.
AppliedReset := BellTrigger.GetResetDelay()
AppliedTransmit := BellTrigger.GetTransmitDelay()
# Fire the bell once at the start of the round to announce the island.
BellTrigger.Trigger()
Pattern 2 — Mutable var tracks a gate, immutable constant defines the limit
Here a lagoon gate opens only a fixed number of times. The limit is immutable; whether the gate is currently open is mutable.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# LagoonGateController — the gate to the hidden lagoon opens MaxOpenings times.
lagoon_gate_controller := class(creative_device):
@editable GateTrigger : trigger_device = trigger_device{}
@editable ResetTrigger : trigger_device = trigger_device{}
# Immutable: the gate opens at most this many times per match.
MaxOpenings : int = 2
# Mutable: tracks whether the gate is currently accepting entries.
var GateEnabled : logic = true
OnBegin<override>()<suspends> : void =
GateTrigger.SetMaxTriggerCount(MaxOpenings)
GateTrigger.Enable()
GateTrigger.TriggeredEvent.Subscribe(OnGateTriggered)
ResetTrigger.TriggeredEvent.Subscribe(OnResetTriggered)
OnGateTriggered(MaybeAgent : ?agent) : void =
Remaining := GateTrigger.GetTriggerCountRemaining()
if (Remaining <= 0):
# No more openings — disable and record the mutable state.
GateTrigger.Disable()
set GateEnabled = false
OnResetTriggered(MaybeAgent : ?agent) : void =
# Re-enable the gate and restore the immutable max.
GateTrigger.Enable()
GateTrigger.SetMaxTriggerCount(MaxOpenings)
set GateEnabled = true
Pattern 3 — Querying device state before acting
Sometimes you want to read the current trigger budget before deciding whether to fire at all — a guard pattern that keeps immutable intent clear.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# CliffSignalController — a signal flare on the clifftop fires only if budget remains.
cliff_signal_controller := class(creative_device):
@editable SignalTrigger : trigger_device = trigger_device{}
@editable WatchTrigger : trigger_device = trigger_device{}
# Immutable flare budget for the whole match.
FlareBudget : int = 4
OnBegin<override>()<suspends> : void =
SignalTrigger.SetMaxTriggerCount(FlareBudget)
WatchTrigger.TriggeredEvent.Subscribe(OnWatchTriggered)
OnWatchTriggered(MaybeAgent : ?agent) : void =
# Read remaining budget — immutable constant set the ceiling,
# mutable device state tells us where we are now.
Budget := SignalTrigger.GetTriggerCountRemaining()
if (Budget > 0):
if (A := MaybeAgent?):
SignalTrigger.Trigger(A)
else:
SignalTrigger.Trigger()
Gotchas
1. Forgetting set when mutating a var
Verse requires the set keyword to reassign a var. Without it, you're creating a new immutable binding that shadows the field — the original var is unchanged and the compiler may warn you.
# WRONG — creates a new local binding, does NOT update the field:
ShotsRemaining = ShotsRemaining - 1
# CORRECT:
set ShotsRemaining = ShotsRemaining - 1
2. GetTriggerCountRemaining returns 0 when the limit is unlimited
If you call SetMaxTriggerCount(0) (which means no limit), GetTriggerCountRemaining() returns 0 — not infinity. A guard like if (Remaining <= 0) will incorrectly disable an unlimited trigger. Always pair a GetMaxTriggerCount() check:
Max := SignalTrigger.GetMaxTriggerCount()
Remaining := SignalTrigger.GetTriggerCountRemaining()
# Only treat 0 as "exhausted" when there IS a limit.
if (Max > 0, Remaining <= 0):
SignalTrigger.Disable()
3. TriggeredEvent sends ?agent, not agent
The event signature is listenable(?agent). Your handler must accept ?agent and unwrap it before calling any agent-typed API:
# WRONG — won't compile, type mismatch:
OnTriggered(A : agent) : void = ...
# CORRECT:
OnTriggered(MaybeAgent : ?agent) : void =
if (A := MaybeAgent?):
# use A here
4. SetMaxTriggerCount clamps to [0, 20]
Passing a value greater than 20 silently clamps to 20. If your design needs more than 20 trigger firings, you'll need to re-enable and reset the counter in a loop, or manage the count yourself in a var.
5. message vs. string for localized text
If you ever display trigger feedback to players via a UI device, remember that message-typed parameters require a localized value. Declare a helper:
ShotMsg<localizes>(S : string) : message = "{S}"
Then pass ShotMsg("Cannon fired!") — never a raw string literal where message is expected.
6. int and float do not auto-convert
SetResetDelay takes a float. Passing an int literal like 5 will fail — write 5.0 explicitly. Similarly, SetMaxTriggerCount takes int, so don't pass 3.0.