Verse Wiki — the Verse handbook for Blueprint authors
Chapter 6 · Lesson 21
struct & interface: Parcels of Values, Contracts of Behavior
class hands you a "remote control" pointing at an object; struct hands over the whole "parcel" — assignment means a copy, and the copies never bother each other. interface goes one further: it won't even give you an object, just a paper "contract". This lesson draws the line between value semantics and reference semantics, then uses one pickup contract to let coins, potions and other totally unrelated items be handled by a single piece of code.
1. struct: A Data Parcel That Copies Whole
Last lesson's class is the Blueprint class you already know: for an instance placed in the level, what you actually hold is a "reference" (the remote control) — assign that reference variable to another variable and only the reference is copied; both wires point at the same Actor. But well over half your game data doesn't need to "share one Actor" at all: a coordinate, a damage record, a stats snapshot… they are just a few numbers lumped together, and copying them around with each copy doing its own thing is the normal case. For that, Verse has struct — the very kind of Structure asset you right-click-create in Blueprints:
coord.verse
# struct: a lightweight data parcel; every field is a constant
coord := struct:
X:int = 0
Y:int = 0
# Instantiate with archetype syntax just like class - no new
Origin := coord{}
Target := coord{X := 3, Y := 4}
The code above simply creates a coord structure asset with two int fields X and Y (both defaulting to 0), then builds two instances: an empty coord{} (all fields at their defaults) and coord{X := 3, Y := 4} (fields filled in inside the braces). Defining one works almost like a Blueprint class: lowercase snake_case name, each field with a type and default, and any field without a default must be supplied inside the braces at instantiation, no exceptions. The real watershed is "what exactly happens the moment you assign": struct copies the whole parcel, class copies the reference. Assign a struct to another variable, pass it as a parameter, stuff it into an array — every single time the whole parcel is duplicated; from then on the two copies part ways for good, and touching either one never reaches the other. Don't take our word for it — the lab rig below lines up the two temperaments; click "Run next step" and see for yourself:
copy_lab_device.verse
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
# hero is a class: reference semantics
hero := class:
var Health:int = 100
# coord is a struct: value semantics, no var fields allowed
coord := struct:
X:int = 0
copy_lab_device := class(creative_device):
OnBegin<override>()<suspends>:void =
HeroA := hero{}
HeroB := HeroA
set HeroB.Health = 10
Print("HeroA health: {HeroA.Health}")
P1 := coord{X := 1}
var P2:coord = P1
set P2 = coord{X := 99}
Print("P1.X = {P1.X} / P2.X = {P2.X}")
play
Output log
Click "Run next step" to watch the code execute line by line.
In the same stretch of code, the class edit "strikes from a distance" while the struct edit says "nothing to do with me". Keep this picture: class copies the remote, struct copies the parcel — every which-one-do-I-pick question in the rest of this chapter starts from that one line.
2. The Three House Rules of struct
A struct is not a "shrunken Blueprint class"; it is deliberately designed as a bundle of data that cannot change once packed. To stay that way it enforces three house rules, each one backed by Compile (that compile button at the top-left of the Blueprint editor) — break one and you go red on the spot:
▢ Rule 1: fields may not carry var (no mutable fields). Put a changeable field var Count:int = 0 into a struct and Compile hurls error 3607 at you: "Structs may not contain mutable members". Struct fields are all locked down — you simply never get to wire a Set node to them. Want to "change" a struct? No such thing — you can only rebuild the whole parcel with the new values, while the old parcel stays untouched. This is not a missing feature; it is on purpose: a bundle of data that never changes is forever safe to copy around.
▢ Rule 2: no functions inside. A struct's braces may only hold data fields; Blueprint functions need not apply. To hang some behavior off a struct, Verse's route is the extension method from Lesson 19 — a function defined separately outside the struct that nevertheless calls like one of its own members:
coord_ext.verse
coord := struct:
X:int = 0
Y:int = 0
# No methods inside a struct body - extension methods add them from outside
# Also demonstrates an immutable update: return a fresh parcel
(P:coord).Mirrored():coord =
coord{X := P.Y, Y := P.X}
Reading the code: the top builds a coord struct (two fields, X and Y). The line (P:coord).Mirrored():coord below bolts a function onto coord from the outside, named Mirrored, whose output is another coord: it swaps the incoming parcel's X and Y, builds a brand-new parcel to return, and never touches the original — exactly what Rule 1 meant by "no editing, only rebuilding".
▢ Rule 3: no direct comparison. Structs are not on Verse's "comparable types" list: two structs cannot be tested for equality with =, and cannot serve as keys of a Map (locker) — force it and you collect a "not comparable" style error. To compare by content, you compare field by field yourself; to key a Map, split out the field that matters (say an int or a string) and key on that.
As compensation, struct enjoys one privilege Blueprint classes can only envy: it can carry the <persistable> marker and act as data kept in cross-session saves (SaveGame) — the "still there when the player quits and rejoins, one copy per player" kind of saved value. Precisely because it is pure data that never changes and has no object identity, it stores away clean. That thread gets reeled back in when Lesson 22 covers the various markers (specifiers).
3. interface: Sign the Contract, Do None of the Work
After struct, keeper of data, meet interface, keeper of behavior. You actually know this one already — it is the code form of a Blueprint Interface (BPI). An interface is a paper contract: it only states which things a signer must be able to do, and says not one word about how. Inside, only a function's "name and pins" (its signature) is allowed — no variable fields, no function bodies (no node graph), not even "implementing half of it first". That is exactly what separates it from an <abstract> class: an abstract class may carry fields and default behavior; an interface is pure contract — terms only, no how-to.
pickup.verse
# The contract: every pickup must report its name and be collectable
pickup := interface:
GetName():string
Collect():void
Reading the code: these three lines create a Blueprint Interface pickup and add two functions to it: GetName (outputs a piece of text, a string) and Collect (no output). Both are declaration only, no implementation — exactly the same act as creating a BPI in the editor, where you only fill in function names and pins and never draw a node graph.
Four rules surround this contract, and every one has a Blueprint counterpart: One, an interface cannot be instantiated — write pickup{} to build one directly and Compile flags red; the contract itself is not an item (just as a BPI asset cannot be dragged into a level). Two, a Blueprint class writes class(pickup) to sign the deal (the same as ticking that interface in Class Settings); once signed, it must implement every single function in the contract, each implementation carrying <override>, with inputs, outputs and every marker matching the interface declaration word for word. Three, a class gets only one parent class but may implement any number of interfaces at once: class(base, iface1, iface2) — the only "multiple inheritance" in Verse, i.e. that long list of Implemented Interfaces you can tick in a Blueprint's Class Settings. Four, an interface can itself inherit several interfaces, stitching small contracts into a big one.
First a warm-up fill-in: put the two keywords back, then click "Check answer":
contract_fill.verse
# A paper contract: only signatures inside, no function bodies (blank 1)
pickup := ____:
GetName():string
# coin signs and delivers: the specifier every interface implementation must carry (blank 2)
coin := class(pickup):
GetName<____>():string = "Coin"
4. Hands-On: One Contract to Handle Every Item
The real power of interfaces is uniform handling. The coin and the potion are two Blueprints with nothing whatsoever in common: one records a face value, the other a heal amount, and neither is the other's child. But once both sign the pickup interface contract, they can lie in the same array and be processed one by one by the same ForEach Loop — the calling side only checks the contract ("as long as you can Collect") and never asks who you really are. This is polymorphism:
loot_demo_device.verse
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
# The contract: every pickup must report its name and be collectable
pickup := interface:
GetName():string
Collect():void
# The coin: one way to honor the contract
coin := class(pickup):
Value:int = 10
GetName<override>():string = "Coin"
Collect<override>():void =
Print("Coin banked +{Value}")
# The potion: a completely different way to honor it
potion := class(pickup):
Heal:int = 25
GetName<override>():string = "Healing Potion"
Collect<override>():void =
Print("Restored {Heal} HP")
loot_demo_device := class(creative_device):
OnBegin<override>()<suspends>:void =
# Element type is the interface: room for every signer
Pickups:[]pickup = array{coin{}, potion{}, coin{Value := 100}}
for (Item : Pickups):
ItemName := Item.GetName()
Print("Picked up: {ItemName}")
Item.Collect()
Reading the code, top to bottom: first build the interface pickup (the contract: can GetName, can Collect); then two unrelated Blueprints coin and potion, both signing the interface and each implementing GetName and Collect with <override> in its own style (the coin logs a deposit, the potion logs healing); finally, in OnBegin (the equivalent of Event BeginPlay), stuff one coin, one potion and one big coin into a single array, and let one ForEach Loop pull them out one by one — call GetName first, then let it Collect. Same loop, different items, each running its own implementation.
Lock onto the line Pickups:[]pickup: the type of each array slot is the interface, so anything that signed the pickup deal — coin, potion, even that big 100-value coin — can squeeze into the same array. Inside the ForEach, which Blueprint's implementation Item.Collect() actually runs is settled by each instance's true identity only once the game is running — add a gem := class(pickup) later and this loop does not change by a single character; the new item just gets handled the same way automatically.
Going the other way, if at some point you genuinely need to know "is this pickup actually a coin?", do a type cast — the Cast To node from Blueprints. It is written coin[Item], the equivalent of "Cast To coin"; the square brackets say this might fail (what if it just isn't a coin?), so it has to sit inside an if (a Branch) — a failed Cast is like a failed dice roll: that wire quietly goes nowhere (takes the else). The snippet below tries to Cast each Item to coin inside the ForEach; on success, TheCoin holds the coin, can read its Value and print it:
cast_demo.verse
for (Item : Pickups):
# On a successful cast, TheCoin is typed coin - its Value is readable
if (TheCoin := coin[Item]):
Print("This is a coin with face value {TheCoin.Value}")
But don't rush to Cast everywhere: most of the time, putting the "behavior that differs" into each Blueprint's own interface implementations (those <override>s) and letting polymorphism dispatch automatically is far more elegant than a long chain of if + Cast.
5. Which One When: Decision Table & Common Pitfalls
struct, class and interface each run their own department; picking one comes down to what you actually need:
What you need
Pick
Why
A small bundle of pure data, isolated by copying, persistable
struct
Whole-parcel copies isolate by nature; can carry <persistable> as SaveGame data; no mutable fields, no functions, lightweight
Changeable state + Blueprint functions + inheritance + object identity (a reference)
class
Reference (remote-control) semantics; Set-able fields, Blueprint functions, single inheritance and <unique> all live here
Only a promise of "what it can do", so unrelated Blueprints get uniform polymorphic handling
interface
Pure contract, crosses separate inheritance lines; one Blueprint can tick any number of interfaces
Now let's sweep this lesson's most frequent compile errors:
▢ Pitfall 1: stuffing a changeable (var) field into a struct. Error 3607 "Structs may not contain mutable members" — nearly every newcomer steps on it once. Want a field you can change? Either switch to a Blueprint class (class), or embrace the "rebuild the whole parcel" style of update that never touches the original and only produces a new one.
▢ Pitfall 2: writing a function inside a struct. Instant error. Route behavior through bolt-on extension methods — or simply ask yourself: should this have been a Blueprint class (class) from the start?
▢ Pitfall 3: "I definitely changed it but nothing happened." You pull a struct out of an array or Map, change it, and the one in the container hasn't moved — of course: what you got the moment you pulled it out was already a copy. To truly update the one in the container, build a whole new parcel with the new values and slot it back where it was.
▢ Pitfall 4: forgetting <override> when honoring the contract, or a mismatched signature. Implementing an interface function requires <override> (the same idea as Blueprint's Override), and inputs, return type and every effect marker (such as <decides>) must match the interface declaration to the letter — one mismatch and Compile fails.
▢ Pitfall 5: trying to build an interface directly.pickup{} gets rejected by Compile on the spot, same as building an abstract class — what can be instantiated is always a concrete Blueprint that signed the deal, never the contract itself.
▢ Pitfall 6: wrong brackets on the Cast.coin(Item) with parentheses is a function call, not a cast; coin[Item] with square brackets is the Cast To — and it must sit inside an if (Branch); written bare at top level, it flags red just the same.
Blueprint Cross-Reference
Both struct and interface already have ready-made asset types in Blueprint — you have used them. The real difference is what happens at the moment of assignment.
In Blueprint
In Verse
Difference
Content Browser → right-click → Blueprint → Structure (a new Structure asset)
coord := struct:
One line replaces an asset. Blueprint struct members can be changed freely; Verse struct fields may never carry var — to "change" one you rebuild the whole parcel
Fill in a struct member's default value in the Details panel
X:int = 0
The syntax maps one to one, but a field without a default is mandatory in Verse: skip it at instantiation and Compile goes red
Break Struct / Make Struct nodes
P.X to read a field; coord{X := 3, Y := 4} to build a new one
Break hands you a copy and Make hands you a new value on both sides; Verse simply writes those two nodes as a dot and a pair of braces
Assigning a struct variable to another variable (value semantics)
var P2:coord = P1
Both copy the whole parcel — the one semantic a Blueprint author never has to relearn. The trap sits in the row below: assigning a class copies only the reference
Assigning an Object Reference variable (reference semantics)
HeroB := HeroA (where hero is a class)
The same :=: a struct on the left splits in two, a class on the left shares one object. Blueprint reminds you through pin colors and icons; Verse relies on you remembering whether that type was declared struct or class
Both are "signatures only, no implementation". The difference: a BPI asset still opens an empty graph in the editor, while a Verse interface body has no room for even one line of function body
Class Settings → Implemented Interfaces, ticking an interface
coin := class(pickup): — interfaces sit in the same parentheses as the parent
Verse puts "parent class" and "interfaces" in one pair of parentheses: the first entry may be a class, the rest are interfaces. Still one parent only; interfaces, tick as many as you like
Implementing an interface function (double-click it under Interfaces, draw the graph)
GetName<override>():string = "Gold Coin"
Blueprint lists the functions awaiting implementation for you; Verse expects you to write them all out, each carrying <override>, signatures matching to the letter
The Cast To node (with its Cast Failed execution pin)
if (TheCoin := coin[Item]):
Square brackets mean this step may not go through, and failure takes the else — that is the Cast Failed pin. Verse forces it to live inside an if; you cannot leave the failure pin unwired the way Blueprint lets you
One difference in this lesson is worth all the others: in Blueprint, "struct vs object reference" is written into the variable's type, and you spot it instantly from pin color and icon; in Verse it is written at the type's definition site, possibly hundreds of lines from the code you are reading. Whether B := A splits or shares depends entirely on whether that type was originally declared with struct or class. Which is exactly why Verse outlawed var inside structs — if you might get it wrong, make getting it wrong cost nothing: an immutable parcel is safe to copy any number of times.
The interface side, by contrast, is almost a straight port. A Blueprint Interface was already "terms only, no how-to"; Verse merely moved that tick list from Class Settings into the parentheses of class(...). The one thing to readjust to: Blueprint lays the unimplemented interface functions out in your function list, and skipping one just means nothing happens at runtime; Verse stops you at Compile and lets you skip none.
Level Challenge
Three challenge gates to certify this lesson's loot: copy semantics, struct house rules and interface rules, one question each. Wrong answers cost nothing; retry as often as you like.
Build a struct P1 (X=1), assign it whole to P2 (var P2:coord = P1), then swap P2 wholesale for a new struct (set P2 = coord{X := 99}). What is P1.X now?
Add a changeable field var Count:int = 0 to a struct — what happens?
Which statement about interface is true?
Further Reading
Technique · EXTRA
When a struct Won't Change: Immutable Data Patterns in Practice
Three ways out of error 3607: rebuild with new values (functional update), upgrade to a Blueprint class, wrap it in functions — plus a complete recipe for "updating one struct inside an array".
subtype, castable, and Why Types Can't Be Compared
Once you try to stuff "a type itself" into a variable as a value, you hit the hardest wall in how Verse judges types — and the Rice's theorem standing behind it.
Interface-Driven Design: A Walkthrough of Epic's Community Tutorial
When should you extract an interface? The community tutorial's complete example, plus real cases of the engine's built-in interfaces, distilled into a decision checklist.