Verse Wiki — the Verse handbook for Blueprint authors
Reference

Verse Syntax Cheat Sheet

Every Verse move on one page: declarations, operators, control flow, containers, functions, types, specifiers, concurrency, the device skeleton, the Scene Graph component skeleton and persistence. Each block gets a minimal snippet, a "What it is in Blueprint" table, and a lesson number. Stuck mid-code? Come back for a quick scan, then follow the lesson number to fill the gap.

How to Use This Page

This is the page you keep open on the second screen while you write. Every section has the same three layers: a minimal snippet that compiles, a ▢ Walk through it paragraph in plain language, and a What it is in Blueprint table — you already know how to do the thing, you just don't know how it's spelled over here.

Looking up a specific term rather than a syntax block? Go to the Blueprint ↔ Verse Glossary. Want to understand why a rule looks the way it does? Follow the lesson number at the end of each section back to the lesson map.

1. Declarations: Variables, Constants & Type Annotations

variables.verse
# Constants: defined once, fixed for life; := lets the compiler infer the type
MaxHealth:int = 100
Nickname := "Bubbles"

# Variables: declared with var and a type annotation; every change must go through set
var Health:int = 100
set Health = 80
set Health += 15

▢ Walk through it: the top half creates two locked-for-life constants (like creating a variable and never wiring a Set node to it); the bottom half's var Health is a variable you can change — and every change has to go through set (that's your Set node). Remember the three symbols: := only handles the very first definition, set … = is how you change the value afterwards (wiring a Set), and a lone = is an equality check that can fail — and there is no == at all. Details in Lesson 8.

types.verse
Count:int = 42                        # integer
Speed:float = 3.5                     # float; literals must carry the decimal point
Title:string = "UE6"                  # string; interpolate with "HP: {Count}"
var Ready:logic = false               # boolean: true / false
Names:[]string = array{"Bubbles", "Archie"}   # array
Table:[string]int = map{"key" => 1}    # map
var MaybeN:?int = false               # option; false means empty
Pair:tuple(int, float) = (1, 2.0)     # tuple

▢ Walk through it: every line reads "name:type = value" — you decide up front what the variable holds (integer, float, text, Boolean, array, map, a maybe-empty option box, a tuple), just like picking the variable type first when you create a variable in Blueprint. Two traps: int and float never auto-convert — go int → float by multiplying by 1.0, and float → int with the might-fail Floor[X] (round down); and always write the decimal point in float literals. Numbers in Lesson 9, strings in Lesson 10, logic in Lesson 11.

The Verse in this sectionWhat it is in BlueprintThe difference in one line
Name:type = valueCreating a variable in the panel and filling in Default ValueNo panel — whatever indent a declaration sits at is the scope it belongs to
Writing the variable's nameGet nodeReading a value needs no node at all
set X = …Set nodeDrop the set and a lone = becomes an equality test
Writing nothing (immutable by default)Blueprint has no "immutable variable"The default is flipped: in Verse you ask for var
int / float / logic / stringInteger / Float / Boolean / StringBoolean is renamed logic and can't be an if condition on its own
?int (an option)A possibly-null object reference"Might not be there" is written into the type, checked at compile time

2. Operators

operators.verse
if (A = B, B <> C):            # equality is =, inequality is <>; there is no ==
    Print("The comma acts as and")
if (X > 0 and Y > 0):          # logic words and / or / not; no && || !
    Print("The whole comparison family is failable expressions")
if (Ready?):                   # postfix ?: succeeds only if the logic is true
    Print("A bare if (Ready) will not compile")

▢ Walk through it: all three ifs look like a Branch, but note what they're asking is not "is this Boolean true" — it's "does the thing in the parentheses go through". So equality is a single =, inequality is <> (there is no ==), logic is spelled and / or / not (no && || !), and a comma works as and too; to use a Boolean as a condition, tack on a ?. In one line: these comparisons don't spit out true/false — they join the flow as "goes through / doesn't", so they can only sit where failure is allowed. Details in Lesson 13.

Symbol What it does What it is in Blueprint Notes
:= Define and initialize Creating a variable and filling in its default First definition only; the type can be omitted
set X = … Assign to a var Set node Compound assignment += -= *= /= (no /= for int)
= <> < <= > >= Comparison, failable Equal / Not Equal / Less / Greater nodes Only allowed inside failure contexts; there is no ==
+ - * / Arithmetic Add / Subtract / Multiply / Divide nodes int / int is failable and yields rational (a fraction type): if (Half := Floor(X / 2)):
Mod[A, B] Remainder Modulo node There is no %; square brackets = failable call
and or not Combine success/failure AND / OR / NOT nodes No && || !; effects inside not always roll back, and it cannot be used to flip a logic variable
X? Query Feeding a Boolean into Branch's Condition / Is Valid Succeeds if the logic is true / the option is non-empty
X[…] Failable call and lookup That Cast Failed pin Indexing, map lookups, <decides> calls and casts all use it

▢ Read the table with Blueprint eyes: := creates a variable and gives it its first value; set X = … is a Set node changing a value; that row of comparison symbols only works where failure is allowed; in arithmetic, integer divided by integer is failable and produces a fraction, so Floor brings it back to an integer; remainder isn't % but Mod[A, B]; logic connectives are only the three words and / or / not; and the trailing X? translates a Boolean or a box into "goes through / doesn't".

3. Control Flow: if / for / loop / defer / case

if.verse
# The condition slot is a failure context: if any step in the chain fails, the whole thing takes else
if (Player := player[Agent], Fort := Player.GetFortCharacter[]):
    Print("Got the character, carrying on")
else:
    Print("Some step in the chain failed")

Max := if (A > B) then A else B    # if is an expression; you can take its value

▢ Walk through it: the upper if is like a chain of Casts — first convert the Agent to a player, then fetch the character entity from it; if any link doesn't go through, the whole thing takes the else. Below it, if (A > B) then A else B uses if as a pick-one-of-two (think Select node) to compute the larger value directly. The key temperament: a "place that allows failure" is rollbackable — any value changed with Set inside the condition is undone wholesale on failure, as if it was never touched, which is also why irreversible actions like Print String aren't allowed inside the condition parentheses. Details in Lesson 12.

for.verse
for (Item : Items):                # iterate over the array's elements
    Print("{Item}")
for (Index -> Elem : Items):       # with the index, starting at 0
    Print("No. {Index}: {Elem}")
for (N := 1..5, N > 2):            # range includes both ends; a failed filter only skips that round
    Print("{N}")
Squares := for (X := 1..5) { X * X }   # for is an expression; collect into a new array

▢ Walk through it: four flavors of ForEach — take each element; take the index along with it; run over a numeric range (with an optional filter bolted on — a round that doesn't pass just gets skipped); and collect each round's result into a fresh array. Note that Verse's ForEach has no break / continue: to "skip a round", write it as a filter condition; to "collect as you go", just use the whole for's result as an array. Details in Lesson 14.

loop.verse
var Round:int = 0
OnBegin<override>()<suspends>:void =
    loop:
        set Round += 1
        if (Round > 3):
            break              # break belongs to loop only; not allowed in for
        Sleep(1.0)             # at least one suspension point per round, or it counts as an infinite loop

▢ Walk through it: loop is an execution wire that circles back on itself and repeats forever (your While Loop); each round bumps the counter by 1, breaks out once it passes 3, and ends with a Sleep(1.0) (a Delay) so it rests a second before the next lap. Two iron rules: break belongs to loop alone — you can't use it in a ForEach; and if a synchronous loop neither breaks nor takes a Delay, you trigger the infinite-loop error ErrRuntime_InfiniteLoop and every bit of Verse in the project goes on strike. Details in Lesson 15.

defer.verse
Countdown()<suspends>:void =
    Print("Countdown started")
    defer:
        Print("Runs only when leaving the scope — even on an early exit")
    Sleep(3.0)
    Print("Three, two, one")

▢ Walk through it: the line inside defer is the wrap-up wire that is guaranteed to run last, right before this function exits — so even though it's written in the middle, it actually executes at the end. The catch: execution has to pass the defer first and check it in for it to count; if it's hiding on a branch that never gets walked, it won't fire. Details in Lesson 15.

case.verse
game_phase := enum{Lobby, Battle, Podium}

PhaseLabel(Phase:game_phase):string =
    case (Phase):
        game_phase.Lobby => "Lobby"
        game_phase.Battle => "Battle"
        game_phase.Podium => "Podium"

▢ Walk through it: case is Blueprint's Switch on Enum — it branches on whichever member the enum currently is, here turning each of the three game phases into a label to hand back. The payoff: cover every member of the enum and you don't need an "other" fallback branch; miss one, and the compiler names it the moment you hit Compile. Branching on integers or strings also uses case, but those can't be exhausted, so they need a _ => fallback. Details in Lesson 11.

The Verse in this sectionWhat it is in BlueprintThe difference in one line
if (…): … else: …BranchTakes "does this step go through", not a Boolean; failed casts, out-of-range indices and missing keys all land in the same else
if (…) then A else BSelect nodeif is an expression and evaluates to a value
Lines at the same indentSequence nodeNo equivalent node — indentation is the ordering
for (X : Arr) / for (I -> X : Arr)ForEach Loop (with or without Index)The whole for accumulates a new array as it runs
for (I := 0..9)For LoopSame keyword as ForEach; the range includes both ends
for (X : Arr, X > 0)A Branch inside the loop bodyThis is Verse's continue; there's no break inside a for
loop: + breakWhile LoopNo while; the condition moves into the body, and a synchronous loop that never pauses trips the infinite-loop error
defer:No equivalent nodeRegister once, and it runs whichever exit you take
case (X):Switch on Enum / Switch on IntEnums must be covered exhaustively, or add a _ =>

4. Containers: array / map / option / tuple

array.verse
var Numbers:[]int = array{1, 2, 3}
if (First := Numbers[0]):            # indexing is failable; it must live inside an if
    Print("The first one is {First}")
if (set Numbers[0] = 9) {}           # changing an element can fail too
set Numbers += array{4}              # append: concatenates into a new array
Print("{Numbers.Length} in total")   # .Length never fails

▢ Walk through it: build an array; grab element 0, but that step "might not go through", so it's wrapped in an if; changing an element and appending follow the same pattern (appending really builds a new array); and .Length is the safe one that never fails. The point: Verse arrays are read-only values with no .Add / .Push in-place mutation, and an out-of-range index doesn't crash — that step just fails and takes the else. Details in Lesson 16.

map.verse
var Scores:[string]int = map{"Bubbles" => 10}
if (Score := Scores["Bubbles"]):     # lookups can fail: no such key takes else
    Print("Found {Score} points")
if (set Scores["Archie"] = 0) {}     # even writes go inside an if
for (Name -> Score : Scores):       # iterates in insertion order
    Print("{Name}: {Score}")

▢ Walk through it: build a "name → score" register (a Map); looking a name up "might come back empty", so it's wrapped in an if; even writing a new key needs an if; and finally a ForEach reads every pair out in insertion order. Don't mix up the two arrows: => pairs entries when building, -> reads them out when iterating. There's no Remove — deleting means filtering to what you want to keep and rebuilding. Details in Lesson 17.

option.verse
var MaybeScore:?int = false          # false is the "empty box"
set MaybeScore = option{42}          # put a value in with option{}, not option()
if (Score := MaybeScore?):           # postfix ? unwraps; fails if empty
    Print("The box holds {Score}")
MaybeFirst := option{Numbers[0]}     # jar a failable expression up into an option

▢ Walk through it: ?int is a box that may hold an integer or may be empty, and false is the empty box; put something in with option{42}; take it out by adding a ?, which doesn't go through on an empty box and takes the else. The last line drops a failable lookup straight into option{…} — because option{…} is itself a place that allows failure: if the step inside fails, you don't get an error, you get an empty box. That makes it the official bridge between "didn't go through" and "has no value". Details in Lesson 18.

tuple.verse
Point:tuple(int, float) = (1, 2.0)   # fixed-length bundle of mixed types
X := Point(0)                        # parenthesis access, checked at compile time, cannot fail

GetXY():tuple(float, float) = (3.0, 4.0)   # multiple return values = return a tuple
Result := GetXY()
Print("x = {Result(0)}, y = {Result(1)}")

▢ Walk through it: bundle "an integer plus a float" into a small package, built with parentheses and indexed with parentheses too — Point(0) — because how many items it holds and what type each one is are fixed at compile time, so the lookup can't fail. GetXY() below shows a tuple's biggest use: letting one function hand back several values at once. Remember: tuples index with parentheses T(0), arrays with square brackets A[0], and a tuple's index must be a literal, never a variable. Details in Lesson 18.

The Verse in this sectionWhat it is in BlueprintThe difference in one line
[]int / array{…}ArrayAn immutable value with no Add; indexing can fail, and out of range doesn't crash
[string]int / map{…}Map (TMap)Both reads and writes can fail; no Remove, so deleting means rebuilding
(none)Set (TSet)Verse provides no set type; the idiom is [t]logic
?t / option{…}A possibly-null object reference + Is Valid"Empty" is written into the type, so Accessed None crashes don't exist
tuple(int, float)A function's several output pinsIndex with parentheses, and the index must be a literal

5. Function Definitions

functions.verse
Add(A:int, B:int):int = A + B        # implicit return: the last expression is the result

Award(?Points:int = 1):void =        # ? prefix = named optional parameter
    Print("Adding {Points} points")
# Calls: Award() uses the default; Award(?Points := 10) passes by name

Twice(F(X:int):int, N:int):int = F(F(N))   # functions can be passed as parameters too

▢ Walk through it: Add is a perfectly ordinary Blueprint function whose last computed value is automatically the return value (no Return node needed). Award demonstrates an optional parameter with a default (prefix the name with ?), which you can skip or pass by name. Twice goes further: a function can be handed to another function as a parameter. Two traps: incoming parameters are read-only, so copy one into a local var before changing it; and writing a function name Foo without parentheses gives you the function itself, not its result. Details in Lesson 19.

decides_fn.verse
# <decides> must appear paired with <transacts>: failure has to be able to roll back
IsBig(X:int)<transacts><decides>:void =
    X > 100

if (IsBig[200]):                     # failable functions are called with square brackets
    Print("Parentheses call normal functions, square brackets call failable ones")

▢ Walk through it: adding <decides> turns a function into one that "might not go through" — if X > 100 inside doesn't pass, the whole function fails. Such a function must also carry <transacts> (rollbackable), and calls switch to square brackets, IsBig[200] (parentheses for normal functions, square brackets for failable ones). One trap: you may not write return inside such a body; the idiom for "find it and leave with the value" is to collect into an empty box var Ret:?t = false and unwrap it on the last line with Ret?. Details in Lesson 19 and Lesson 22.

The Verse in this sectionWhat it is in BlueprintThe difference in one line
Foo(A:int):int = …FunctionThe last expression is automatically the return value — no Return node
?Points:int = 1A default value on an input pinOnly named parameters carrying ? can have defaults
Passing a function as a parameterBlueprint basically can'tFunctions are first-class values in Verse
<decides> + Foo[]No equivalent; a cast's two exits is closest"Can fail" is part of the signature and the compiler enforces it
<computes> / <converges>The Pure checkboxVerse checks whether your declaration is true
<public> functions at module scopeBlueprint Function LibraryNo "library" asset needed — folders are modules
(none)MacroAn ordinary function is enough
(none)The By Ref checkboxParameters are read-only; copy into a local var first

6. Type Definitions: class / struct / interface / enum

class.verse
enemy := class:
    Name:string                      # no default → must be filled in at instantiation
    var Health:int = 100

    TakeDamage(Amount:int):void =
        set Health -= Amount         # use Self inside methods to mean "myself"

Grunt := enemy{Name := "Grunt"}      # archetype instantiation; there is no new

▢ Walk through it: this is defining a Blueprint class — list the fields (any without a default must be filled in when building an instance), write the methods (use Self inside them for "me"), and finally enemy{Name := "Grunt"} builds an instance (no new — just braces with the fields filled in). Inheritance is written child := class(base): (your Child Blueprint Class), overriding a parent member must carry <override> (that Override dropdown), and calling the parent's original is (super:)Method(); classes have "reference" semantics (you pass around the same instance, not a copy). Details in Lesson 20.

struct.verse
point := struct:
    X:float = 0.0
    Y:float = 0.0

P := point{X := 3.0, Y := 4.0}       # value semantics: assignment and passing both copy
Q := point{X := P.X, Y := 9.9}       # "modifying" = constructing a new instance

▢ Walk through it: struct is the Structure asset you create by right-clicking — a small bundle of pure data. It has value semantics: Q := point{X := P.X, …} looks like "tweak one field" but really builds a whole new instance from the old one, and assignment and parameter passing copy the whole bundle without affecting each other. The rules: no mutable var fields inside a struct and no methods — to give it behaviour, hang an extension method on it from outside, (S:point).Norm():float = …. Details in Lesson 21.

interface.verse
rideable := interface:
    Mount():void                     # signature only, no implementation

bicycle := class(rideable):
    Mount<override>():void =         # every interface method you implement needs <override>
        Print("Hop on, let's go")

▢ Walk through it: interface is the Blueprint Interface (BPI) — signatures only, no implementation; the class below adopts it, so it has to supply Mount, and implementing an interface method also carries <override>. The rules: a class has exactly one parent but can adopt several interfaces, class(base, iface1, iface2); and to downcast a parent reference to a concrete subtype you write if (C := child_type[Base]): — a step that "might not go through", which is Blueprint's Cast. Details in Lesson 21.

enum.verse
card_suit := enum{Clubs, Diamonds, Hearts, Spades}

MySuit := card_suit.Hearts           # members are reached through the type name
if (MySuit = card_suit.Hearts):      # only = and <> equality checks are supported
    Print("It's Hearts")             # want text? write your own case mapping

▢ Walk through it: enum is the Enumeration asset you create by right-clicking, listing a few named options; you reach a member as "type name dot member name", and you test for one with = (only = and <> are supported). The point: enum members hide no integer behind them — no conversion to numbers or text, and no ordering comparisons; to get display text, write your own case mapping. Definitions go outside the class, at module scope. Details in Lesson 11.

The Verse in this sectionWhat it is in BlueprintThe difference in one line
classBlueprint Class assetNot an asset but a declaration in a text file; reference semantics, single inheritance
class(base)Child Blueprint ClassOverriding a member requires <override>
my_class{Field := 3}Spawn + Expose on SpawnNo new; fields without defaults are required
structStructure assetStricter: no var fields and no methods
interfaceBlueprint Interface (BPI)No "Message call vs direct call" split
enumEnumeration assetNo hidden integer; no conversion to number or text
child_type[Base]Cast To …Square brackets = might fail, and failure takes the else

7. Specifiers & Attributes

What sits in angle brackets <…> is a specifier, and the compiler checks you against it the moment you hit Compile; what starts with @ is an attribute, written on the line above a declaration, mostly affecting how things look in the UEFN editor (for instance @editable floats a field up into the Details panel). Pick out the ones you'll actually use.

Modifier In one line What it is in Blueprint Where it goes
<public> <internal> <protected> <private> Access level; the default is internal (visible within the module), not public The Private / Protected / Public dropdown After the identifier; on a var it splits read from write: var<protected> Ammo<public>:int
<override> Overrides a parent member; mandatory The Override dropdown on a function After the member name
<abstract> Can't be instantiated; may contain unimplemented methods The Abstract class checkbox class<abstract>
<final> The class can't be inherited from / the member can't be overridden Marking a function as non-overridable class or member
<concrete> Every field has a default, so my_class{} constructs it empty-handed; required for custom @editable classes "This class can go straight into the panel" class<concrete>
<unique> Instances have identity, so they compare with = and work as map keys (which is how player works) "Is this the same object reference?" class<unique>
<transacts> Actions can roll back; the only kind a failure context welcomes Blueprint has none: a Set is spilt milk Effect: after the parameter list, before the return type
<decides> Failable, called with square brackets; must be paired with <transacts> A cast's two exits is the closest thing Effect
<suspends> Async, can suspend across frames; only callable directly from an async context Latent nodes (the ones with the little clock) Effect
<computes> <varies> <converges> Three grades of "no side effects", pick one; write nothing and you get no_rollback The Pure checkbox Effect
<persistable> Eligible for saving; the class must be <final> and hold only constant fields The fields of a SaveGame Object After class / struct
<localizes> Wraps an interpolated string into a message (the type UI text wants) Text (localizable) Function / constant
<final_super> Scene Graph requirement: a custom component must derive directly from component. Beta — the official docs are the authority. No Blueprint counterpart class<final_super>(component)
@editable Exposes a field to the Details panel; changing the value needs no recompile The Instance Editable eye Attribute: the line above the field declaration

▢ Every keyword in this table can light up red when you hit Compile. A few with Blueprint eyes: <override> = overriding a parent member; <concrete> = every field has a default so you can build it with an empty {}, required for custom @editable classes; <unique> = instances carry identity and can be map keys; <transacts> = actions can roll back, the only kind a "place that allows failure" welcomes; <suspends> = waits across frames; @editable = the Instance Editable eye. The "effect" family is really just writing "what this function does" into the signature — when an error mentions no_rollback / decides / suspends, come back to this table first. Details in Lesson 22.

8. The Five Concurrency Expressions

Expression Semantics Fate of the other tasks What it is in Blueprint
sync: Continues only when all are done; results bundled into a tuple —— None: Blueprint has to count them back in by hand
race: Ends the moment the first finishes; the value is the winner's Cancelled at their next suspension point None: a timeout in Blueprint is hand-wired through Tick or a Timeline
rush: Returns the moment the first finishes Keep running, wrapped up with the enclosing scope None
branch: Starts it, main line continues Discarded when the enclosing scope exits Closest to "start another execution wire", but someone cleans up
spawn{ Fn() } Releases an independent task, returns a task Not cancelled; runs itself to completion "Start another execution wire", with nobody minding when it ends

All five are ways to run several wires at once. With Blueprint eyes: sync = wait for everyone before moving on; race = first one wins and the rest are cut on the spot (this is your timeout); rush = also takes the first result, but the stragglers aren't cut; branch = split off and carry on, cleaned up automatically when the section ends; spawn = force off a wire nobody minds the lifetime of, and the only one usable from ordinary synchronous code. The first four only work inside a suspending context.

race_timeout.verse
GateOnce()<suspends>:void =
    race:
        block:
            Button.InteractedWithEvent.Await()   # Button is an @editable reference
            Print("Pressed in time!")
        block:
            Sleep(10.0)
            Print("Timed out — moving on")

▢ Walk through it: race makes two wires run against each other — one waits for the button press (Await() suspends until the event fires, with Button being the @editable reference wired up in the panel), the other runs Sleep(10.0) as an alarm clock; whoever gets there first wins, and the loser is cut on the spot. That's the standard timeout recipe: an event racing a clock. When an arm has several lines, wrap it in block: to make it one wire, and the result types of the arms have to be compatible. Details in Lesson 24.

spawn.verse
OnPressed(Agent:agent):void =        # a Subscribe callback is not an async function
    spawn{ CloseAfterDelay() }       # a spawn body must be a single async call

CloseAfterDelay()<suspends>:void =
    Sleep(5.0)
    Print("Five seconds up — wrapping up")

▢ Walk through it: the button's Subscribe callback OnPressed isn't a suspending function itself, so to do something that needs waiting inside it, spawn{ CloseAfterDelay() } splits off a new wire (the braces can only hold a single async call) and returns immediately; that new wire waits with Sleep(5.0) (a five-second Delay) and then wraps up. A reminder: prefer the four structured forms (sync/race/rush/branch) over a bare spawn, and Sleep(0.0) is the standard "continue next frame". Suspension and the flow of time in Lesson 23, the five expressions in Lesson 24.

9. The Device Skeleton: @editable + Subscribe (today, in UEFN)

Today the only place Verse actually runs is UEFN, where "something you can place in a level" is a creative_device. The device below is the minimal skeleton behind every piece of UEFN Verse code.

my_device.verse
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }

my_device := class(creative_device):
    @editable
    Button:button_device = button_device{}    # wire up the real device in the panel

    OnBegin<override>()<suspends>:void =
        Button.InteractedWithEvent.Subscribe(OnPressed)

    OnPressed(Agent:agent):void =
        Print("A player pressed the button")

▢ Walk through it: this is the skeleton of a minimal device — an @editable Button (that little eye in the panel, wired to a real button once it's in the level); OnBegin, which is Event BeginPlay, using Subscribe (Bind Event) right at the start to bind the button's "was interacted with" event to its own OnPressed; press the button and OnPressed runs and prints a line. To make it work: the device has to be dragged from the Content Browser into the level, and the @editable has to be wired to the real thing in the Details panel; the callback's parameters must match what the event hands over (a button gives you an agent); and if you need to wait for something inside the callback, spawn. Your first device in Lesson 6, events and @editable in Lesson 25, the full capstone project in Lesson 29.

The Verse in this sectionWhat it is in BlueprintThe difference in one line
class(creative_device)Blueprint ActorNot an asset but a declaration; it only runs once dragged into the level
An @editable fieldA row in the Details panelThe panel mirrors the code — no marker, no row
OnBegin<override>()<suspends>Event BeginPlayCarries <suspends>, so it can hold a whole game loop
.Subscribe(F)Bind EventReturns a cancelable; bind once, because binding twice stacks
Print("…")Print StringRequires using the Diagnostics module first

10. Scene Graph: entity and component (where UE6 is heading)

Status first: Scene Graph is UE6's new gameplay framework, built on Verse from the ground up, with entities holding components. It ships in UEFN today as Beta, Epic's own documentation says to be careful about shipping with it, and some tools still need enabling under Beta Access in Project Settings. At the same time: Actors and Blueprints are fully supported in UE6 Early Access (targeted at the end of 2027) and the early releases, deprecation waits until Scene Graph is mature enough and has no date, and Epic has committed to conversion tools before deprecation but has not released them yet.

The skeleton below follows the form used in Epic's official documentation. Signatures can change during Beta, so when you write code, the official documentation is the authority (sources are linked at the end of this section).

my_component.verse
using { /Verse.org }
using { /Verse.org/Native }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/SceneGraph }

# A custom component must carry <final_super> and derive directly from component
my_component := class<final_super>(component):

    @editable
    WaitSeconds:float = 3.0          # same as on a device: floats up to the Details panel

    # Runs once when simulation begins (synchronous) — the component's Event BeginPlay
    OnBeginSimulation<override>():void =
        (super:)OnBeginSimulation()

    # Long-lived async logic goes here: wait on time, wait on events, hold a loop
    OnSimulate<override>()<suspends>:void =
        Sleep(WaitSeconds)

    # Wrap-up when simulation ends
    OnEndSimulation<override>():void =
        (super:)OnEndSimulation()

▢ Walk through it: this is the minimal skeleton of a custom component. <final_super> is Epic's hard requirement — a component must derive directly from component, and without it the class can't be added to an entity (subclasses derived further from it don't need to repeat it). @editable works exactly as it does on a device, so that part doesn't have to be relearned. The three lifetime functions cover "begin simulation / long-lived async logic / end simulation", and the official examples call the (super:) version first before doing their own work.

component_entity.verse
# A component already knows which entity it is on: just write Entity
if (Mesh := Entity.GetComponent[mesh_component]):
    # Square brackets = might not go through: this entity may have no mesh component
    Print("Got the mesh component on the same entity")

Parent := Entity.GetParent()         # look upward
Kids   := Entity.GetEntities()       # look downward at child entities
Comps  := Entity.GetComponents()     # the components on this entity

▢ Walk through it: in Scene Graph, "getting hold of something else" is no longer Blueprint's Get All Actors of Class — you walk the entity hierarchy. Inside a component, Entity is a ready-made data member pointing at "the entity I'm attached to", and GetComponent[…] uses square brackets because that component may not be there. The hierarchy is the data structure itself — that's the biggest difference between Scene Graph and the World Outliner.

How it's written in Scene Graph What it is in Blueprint Notes Status
entity Actor A nearly empty container that gains capability from components; a class deriving from entity is what Epic calls a prefab Beta
class<final_super>(component) Actor Component The only home for behaviour and data Beta
OnAddedToScene The component being added to the world One of the lifetime functions Epic lists; the full signature is whatever the official docs say Beta
OnBeginSimulation<override>():void Event BeginPlay (component version) Synchronous one-time setup Beta
OnSimulate<override>()<suspends>:void No equivalent The component's long-lived async logic — loops and event waits Beta
OnEndSimulation<override>():void Event EndPlay (component version) Wrap-up Beta
OnRemovingFromScene The component about to be removed One of the lifetime functions Epic lists; the full signature is whatever the official docs say Beta
Entity GetOwner() A data member on component, pointing at the entity it's attached to Beta
TickEvents.PrePhysics / .PostPhysics Event Tick Hang callbacks on it for per-frame timing; signatures per the official docs Beta
Entity.GetComponent[t] Get Component by Class Square brackets = might not go through Beta
Entity.AddComponents(…) / RemoveFromEntity() Add Component / Destroy Component Only one component of a given type per entity Beta
GetParent() / AddEntities() / RemoveFromParent() Attach To / Detach The hierarchy is the data structure, not just placement Beta
AddTag() / ContainsTag() / RemoveTag() Actor Tag Entities come with a tag interface Beta
SendUp() / SendDown() / OnReceive() Broadcasting a notification along the hierarchy Sends a scene event through the entity tree, picked up by components along the way Beta
IsInScene() / IsSimulating() Is Valid-style state queries Epic describes them as "succeeds if …", i.e. failable calls Beta

Every name and skeleton in this section comes from Epic's official documentation. The API reference page for entity places it in the /Verse.org/SceneGraph module, while the official tutorial's example uses the four using lines shown above — for both, the official documentation is the authority. Epic's pages don't give full signatures for OnAddedToScene and OnRemovingFromScene, so those are listed here as concepts only — don't copy a guessed signature. The full Scene Graph worldview is in Lesson 26, and writing a component is Lesson 27.

Sources

11. Persistence Patterns

persist_coins.verse
# Module scope (outside the class) — the location itself is the persistence switch
var PlayerCoins:weak_map(player, int) = map{}

AddCoins(Player:player, Amount:int):void =
    if (Old := PlayerCoins[Player], set PlayerCoins[Player] = Old + Amount):
        Print("Coins credited — they survive across sessions")

▢ Walk through it: put the coin-holding weak_map outside the class, at module scope — that location is itself the save switch, so the data is per-player and carries across sessions automatically. Inside AddCoins, reading the old value and writing the new one both "might not go through", so the whole statement is wrapped in one if (read the old value and set the new one back in a single step). Remember: the first time a player joins you have to lay down an initial value with if (set PlayerCoins[Player] = 0) {} before there's anything to add to. Details in Lesson 28.

persist_class.verse
player_profile := class<final><persistable>:
    Level:int = 1
    Coins:int = 0

var Profiles:weak_map(player, player_profile) = map{}
# Update = read the old instance → build a new one from its values → set the whole thing back

▢ Walk through it: pack the data you want saved (level, coins) into a <persistable> class, then keep the instances in a player-keyed weak_map; the update pattern is "read the old instance → build a new one from the old values → set the whole thing back". Remember weak_map's three can'ts: you can't ask for a count, can't iterate, and the keys are weak references; the key must be a player — if you're holding an agent, convert it first with player[Agent] (Cast to Player). Data that will grow fields later is safest in a class (after release, only classes can add new fields with defaults). Details in Lesson 28.

The Verse in this sectionWhat it is in BlueprintThe difference in one line
A module-scope weak_map(player, t)SaveGame / Save Game Object"Outside the class" is itself the save switch
class<final><persistable>The structure of a SaveGame ObjectOnly immutable fields, and the class must be <final>
player[Agent]Cast to PlayerSquare brackets = might not go through, so wrap it in an if
(none)Save Game to Slot / Load Game from SlotNo explicit save or load call — reading and writing the weak_map is the save