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
# 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.
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 section | What it is in Blueprint | The difference in one line |
|---|---|---|
Name:type = value | Creating a variable in the panel and filling in Default Value | No panel — whatever indent a declaration sits at is the scope it belongs to |
| Writing the variable's name | Get node | Reading a value needs no node at all |
set X = … | Set node | Drop 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 / string | Integer / Float / Boolean / String | Boolean 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
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
# 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 (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.
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.
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.
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 section | What it is in Blueprint | The difference in one line |
|---|---|---|
if (…): … else: … | Branch | Takes "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 B | Select node | if is an expression and evaluates to a value |
| Lines at the same indent | Sequence node | No 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 Loop | Same keyword as ForEach; the range includes both ends |
for (X : Arr, X > 0) | A Branch inside the loop body | This is Verse's continue; there's no break inside a for |
loop: + break | While Loop | No while; the condition moves into the body, and a synchronous loop that never pauses trips the infinite-loop error |
defer: | No equivalent node | Register once, and it runs whichever exit you take |
case (X): | Switch on Enum / Switch on Int | Enums must be covered exhaustively, or add a _ => |
4. Containers: array / map / option / tuple
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.
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.
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.
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 section | What it is in Blueprint | The difference in one line |
|---|---|---|
[]int / array{…} | Array | An 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 pins | Index with parentheses, and the index must be a literal |
5. Function Definitions
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> 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 section | What it is in Blueprint | The difference in one line |
|---|---|---|
Foo(A:int):int = … | Function | The last expression is automatically the return value — no Return node |
?Points:int = 1 | A default value on an input pin | Only named parameters carrying ? can have defaults |
| Passing a function as a parameter | Blueprint basically can't | Functions 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 checkbox | Verse checks whether your declaration is true |
<public> functions at module scope | Blueprint Function Library | No "library" asset needed — folders are modules |
| (none) | Macro | An ordinary function is enough |
| (none) | The By Ref checkbox | Parameters are read-only; copy into a local var first |
6. Type Definitions: class / struct / interface / enum
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.
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.
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.
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 section | What it is in Blueprint | The difference in one line |
|---|---|---|
class | Blueprint Class asset | Not an asset but a declaration in a text file; reference semantics, single inheritance |
class(base) | Child Blueprint Class | Overriding a member requires <override> |
my_class{Field := 3} | Spawn + Expose on Spawn | No new; fields without defaults are required |
struct | Structure asset | Stricter: no var fields and no methods |
interface | Blueprint Interface (BPI) | No "Message call vs direct call" split |
enum | Enumeration asset | No 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.
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.
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.
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 section | What it is in Blueprint | The difference in one line |
|---|---|---|
class(creative_device) | Blueprint Actor | Not an asset but a declaration; it only runs once dragged into the level |
An @editable field | A row in the Details panel | The panel mirrors the code — no marker, no row |
OnBegin<override>()<suspends> | Event BeginPlay | Carries <suspends>, so it can hold a whole game loop |
.Subscribe(F) | Bind Event | Returns a cancelable; bind once, because binding twice stacks |
Print("…") | Print String | Requires 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).
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.
# 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
# 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.
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 section | What it is in Blueprint | The 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 Object | Only immutable fields, and the class must be <final> |
player[Agent] | Cast to Player | Square brackets = might not go through, so wrap it in an if |
| (none) | Save Game to Slot / Load Game from Slot | No explicit save or load call — reading and writing the weak_map is the save |