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-invector2struct) andGold. This is pure value data — no methods, novar.gold_bank := class:declares a class. Itsvar Total:int = 0is mutable state, andDepositis a method that mutates it withset. 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.@editablefields (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 touchesBanksees the same running total.- In
OnBeginwe build an array of chest structs using archetype syntax (chest_loot{ ... }), then loop andDepositeach chest's gold. The struct is copied each iteration — harmless, since we only readChest.Gold. StartPad.TriggeredEvent.Subscribe(OnPierStepped)wires the trigger to our handler.OnPierStepped(Agent : ?agent)unwraps the optional agent withif (A := Agent?):, then callsAnnouncer.Show(...)with a localizedmessage, reading the sharedBank.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_chestextendstreasure_chest) must use classes. varin a struct is discouraged; mutate via copy. Give mutable, behavior-rich state to a class whereseton avarfield via a method is the clean pattern.- You must declare devices as
@editableclass fields. A bareSomeDevice.Method()won't compile with 'Unknown identifier'. Every placed device is referenced through an@editablefield on yourcreative_deviceclass — because your device is itself a class. messageparams need localized text, not raw strings. DeclareMsg<localizes>(S:string):message = "{S}"and passMsg("..."). There is noStringToMessage.- Unwrap the optional agent. A
listenable(?agent)handler receives(Agent : ?agent); always guard withif (A := Agent?):before using it. - Verse won't auto-convert int↔float. A
vector2usesfloat(X := 100.0), while a gold count is anint— mixing them needs an explicit conversion.