Reference Verse compiles

Immutable by Default: Constants, Variables, and the trigger_device

In Verse, every value you create is locked in place like a clifftop overlooking the sea — it cannot change unless you explicitly ask for mutability with `var`. This immutable-by-default philosophy eliminates whole classes of bugs and makes your island logic predictable. In this article you'll learn what immutability means in practice, when to reach for `var`, and how to wire it all together with a `trigger_device` on a cel-shaded pirate cove.

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

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 float components.

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 var field for mutable runtime state
  • trigger_device methods: SetMaxTriggerCount, GetTriggerCountRemaining, Trigger, Disable
  • trigger_device event: 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.

Guides & scripts that use vector3

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

Build your own lesson with vector3

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 →