Reference Verse compiles

struct vs class in Verse: Pirate Cove Race Tracker

Verse gives you two ways to group data: **structs** (lightweight, value-type bundles — think a snapshot of a player's lap time) and **classes** (reference-type objects with mutable state and behavior — think the race manager that owns the timer and reacts to triggers). Knowing which to reach for is the difference between clean, bug-free island code and a tangle of accidental copies. This article teaches both through a single sun-drenched pirate-cove race: players sprint from the dock, through th

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

Overview

When you write Verse for your island, you constantly need to bundle related data together: a treasure chest's position and gold value, a player's score and name, a wave of loot floating in a cove. Verse gives you two tools for this — struct and class — and picking the right one keeps your code clean and bug-free.

A struct is a plain bag of values. It groups related variables together (like Verse's built-in vector2 with its X and Y). When you copy a struct or pass it to a function, you get a fresh independent copy — changing the copy never touches the original. Structs can't inherit from other structs, and (going forward) their fields should all be public. Reach for a struct for small, value-like data: a coordinate, an RGB color, a score-and-name pair.

A class is a living object with identity. When you pass a class instance around, everyone shares the same instance — change it in one place and the change is visible everywhere. Classes support inheritance (class(base_class)), constructors, methods that mutate internal var fields, and can be marked @editable so you tune them in the UEFN details panel. Reach for a class when you need shared mutable state, behavior, or a hierarchy — like a chest that opens, a player-stat tracker, or a creative_device itself (every device you write IS a class).

The short rule: struct = copied value, class = shared object. Below we build both on a sunny dock so you can feel the difference.

API Reference

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

Walkthrough

Imagine a bright cel-shaded cove. Three treasure chests sit on the dock, each with a spawn location and a gold value. A trigger fires when a player runs onto the pier, and we tally up the gold and announce it. We'll use a struct for the immutable chest position/value data, and a class to hold the mutable running total.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/SpatialMath }

# A STRUCT: plain value data describing one chest.
# Copied by value — safe to pass around, no shared state.
chest_loot := struct:
    Position:vector2
    Gold:int

# A CLASS: shared, mutable running total with behavior.
gold_bank := class:
    var Total:int = 0

    Deposit(Amount:int):void =
        set Total = Total + Amount

cove_treasure := class(creative_device):

    @editable
    StartPad : trigger_device = trigger_device{}

    @editable
    Announcer : hud_message_device = hud_message_device{}

    # One shared bank instance for the whole island.
    Bank : gold_bank = gold_bank{}

    Loot<localizes>(S:string):message = "{S}"

    OnBegin<override>()<suspends>:void =
        # Build an array of chest structs (value data).
        Chests := array:
            chest_loot{ Position := vector2{X := 100.0, Y := 0.0}, Gold := 50 }
            chest_loot{ Position := vector2{X := 200.0, Y := 0.0}, Gold := 75 }
            chest_loot{ Position := vector2{X := 300.0, Y := 0.0}, Gold := 125 }

        # Deposit each chest's gold into the shared class.
        for (Chest : Chests):
            Bank.Deposit(Chest.Gold)

        StartPad.TriggeredEvent.Subscribe(OnPierStepped)

    OnPierStepped(Agent : ?agent):void =
        if (A := Agent?):
            Announcer.Show(Loot("Treasure counted: {Bank.Total} gold!"))

Line by line:

  • chest_loot := struct: declares a struct with two public fields, Position (a built-in vector2 struct) and Gold. This is pure value data — no methods, no var.
  • gold_bank := class: declares a class. Its var Total:int = 0 is mutable state, and Deposit is a method that mutates it with set. Structs can't do this cleanly — a class is the right choice for shared mutable state.
  • cove_treasure := class(creative_device): — the device itself is a class. @editable fields (StartPad, Announcer) let you drag placed devices in from UEFN.
  • Bank : gold_bank = gold_bank{} creates ONE bank instance. Because it's a class, every method that touches Bank sees the same running total.
  • In OnBegin we build an array of chest structs using archetype syntax (chest_loot{ ... }), then loop and Deposit each chest's gold. The struct is copied each iteration — harmless, since we only read Chest.Gold.
  • StartPad.TriggeredEvent.Subscribe(OnPierStepped) wires the trigger to our handler.
  • OnPierStepped(Agent : ?agent) unwraps the optional agent with if (A := Agent?):, then calls Announcer.Show(...) with a localized message, reading the shared Bank.Total.

Common patterns

Struct copy semantics — modifying a copy leaves the original untouched

When you read a struct field into a var and change it, the original struct in your data is unaffected. Here a trigger shows both the original chest value and a doubled local copy.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/SpatialMath }

chest_loot := struct:
    Position:vector2
    Gold:int

copy_demo := class(creative_device):

    @editable
    Pad : trigger_device = trigger_device{}

    @editable
    Board : hud_message_device = hud_message_device{}

    Msg<localizes>(S:string):message = "{S}"

    OnBegin<override>()<suspends>:void =
        Pad.TriggeredEvent.Subscribe(OnStep)

    OnStep(Agent : ?agent):void =
        if (A := Agent?):
            Original := chest_loot{ Position := vector2{}, Gold := 100 }
            # Copy the struct, double the copy's gold.
            var Bonus : chest_loot = Original
            set Bonus.Gold = Original.Gold * 2
            # Original.Gold is STILL 100 — struct copy is independent.
            Board.Show(Msg("Original {Original.Gold}, Bonus {Bonus.Gold}"))

Class shared reference — one instance, many views

Two triggers both talk to the same class instance, so a deposit on one pad shows up when the other pad reads it.

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

score_tracker := class:
    var Points:int = 0
    Add(N:int):void =
        set Points = Points + N

shared_ref_demo := class(creative_device):

    @editable
    ScorePad : trigger_device = trigger_device{}

    @editable
    ReadPad : trigger_device = trigger_device{}

    @editable
    Board : hud_message_device = hud_message_device{}

    Tracker : score_tracker = score_tracker{}

    Msg<localizes>(S:string):message = "{S}"

    OnBegin<override>()<suspends>:void =
        ScorePad.TriggeredEvent.Subscribe(OnScore)
        ReadPad.TriggeredEvent.Subscribe(OnRead)

    OnScore(Agent : ?agent):void =
        if (A := Agent?):
            Tracker.Add(10)

    OnRead(Agent : ?agent):void =
        if (A := Agent?):
            Board.Show(Msg("Shared total: {Tracker.Points}"))

Class inheritance — a base chest and a special golden chest

Only classes can inherit. Here a golden_chest extends treasure_chest and overrides its value.

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

treasure_chest := class:
    Value():int = 50

golden_chest := class(treasure_chest):
    Value<override>():int = 500

inherit_demo := class(creative_device):

    @editable
    Pad : trigger_device = trigger_device{}

    @editable
    Board : hud_message_device = hud_message_device{}

    Msg<localizes>(S:string):message = "{S}"

    OnBegin<override>()<suspends>:void =
        Pad.TriggeredEvent.Subscribe(OnOpen)

    OnOpen(Agent : ?agent):void =
        if (A := Agent?):
            Fancy : treasure_chest = golden_chest{}
            Board.Show(Msg("Chest worth {Fancy.Value()} gold"))

Gotchas

  • Structs are copied, classes are shared. The #1 source of "my value didn't change!" bugs. If you mutate a struct field on a copy, the original is untouched. If you want changes to stick everywhere, use a class.
  • Structs should have only public fields. UEFN now warns when a struct has non-public members — that capability is going away. Need private/protected data? Use a class instead.
  • Structs can't inherit. There's no struct(base_struct). Any hierarchy (golden_chest extends treasure_chest) must use classes.
  • var in a struct is discouraged; mutate via copy. Give mutable, behavior-rich state to a class where set on a var field via a method is the clean pattern.
  • You must declare devices as @editable class fields. A bare SomeDevice.Method() won't compile with 'Unknown identifier'. Every placed device is referenced through an @editable field on your creative_device class — because your device is itself a class.
  • message params need localized text, not raw strings. Declare Msg<localizes>(S:string):message = "{S}" and pass Msg("..."). There is no StringToMessage.
  • Unwrap the optional agent. A listenable(?agent) handler receives (Agent : ?agent); always guard with if (A := Agent?): before using it.
  • Verse won't auto-convert int↔float. A vector2 uses float (X := 100.0), while a gold count is an int — mixing them needs an explicit conversion.

Guides & scripts that use trigger_device

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

Build your own lesson with trigger_device

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 →