Verse Wiki — the Verse handbook for Blueprint authors
Chapter 5 · Lesson 16

array: Line Up Your Loot

Welcome to the Containers chapter. A variable holds exactly one thing; an array holds a whole row — the same thing you get in the Blueprint variables panel when you click the icon next to a variable's type and switch it to the nine-square grid, turning the variable into an Array. This lesson covers: how to pre-fill a row of data, how to grab an item by its number (without crashing when it is not there), how to count how many there are, how to join two rows together, and how to cut out a middle stretch — everything you need to keep a row of data firmly under control.

1. The array{} Literal and the []t Type: Opening a Row of Slots

The variables in earlier lessons were all single-slot backpacks: create a Health in the variables panel and it holds exactly one number. But game data loves to travel in packs — a weapon loadout, a string of kill records, a row of ranked scores. Give each one its own variable? Weapon1, Weapon2, Weapon3… by the tenth you'll want to flip the table. Verse's answer is the array — the same thing as clicking the icon next to a variable's type in the Blueprint variables panel and switching it to the nine-square grid to make it an Array: open a whole row of slots at once, numbered 0, 1, 2, …, all managed under a single name.

loadout_device.verse
using { /Fortnite.com/Devices }

loadout_device := class(creative_device):

    # []string reads as "a row of strings"; array{...} is the array literal
    Weapons:[]string = array{"Pickaxe", "Shotgun", "Sniper Rifle"}

    # An empty array is also array{}; fill it later
    var Kills:[]int = array{}

Read the snippet above: it creates two array variables in the Blueprint variables panel — one called Weapons, holding strings, pre-filled with three weapon names; one called Kills, holding ints, left empty for now to fill later. Two things to remember about the notation: the type is written []string, brackets first, contents after, read as "a row of strings"; to pre-fill, use array{element, element, …} — the keyword array plus a pair of curly braces. Don't swap the curly braces for any other bracket — get it wrong and hitting Compile (Ctrl+Shift+B in Verse) lights up red with an error.

One more iron rule: everything in a row must be the same kind. []int holds nothing but integers, []string nothing but text; want to write array{1, "a"} to mix numbers and text? Hit Compile and it lights up red — rejected. If you genuinely need to bundle values of different types, that's a job for tuples — see Lesson 18.

2. Subscript Access: A Grab That Might Come Up Empty

Slots are open — how do you take something out? Writing Items[0] means "grab slot 0" — numbering starts at 0, so slot 0 is the first one (Blueprint arrays play by the same rule). But here Verse has a design choice with real personality: the act of grabbing a slot can itself come up empty. Which makes sense when you think about it: what if the array holds 3 items and you reach for slot 99? Verse neither crashes nor shoves a default value at you — it treats "nothing there" as a line that doesn't go through, the same idea as a Branch node taking the False pin when its condition doesn't hold. So in Verse, grabbing a slot is like wiring up a Branch: one line for when you get it, another for when you don't.

So in Verse, grabbing a slot always needs its "what if there's nothing there" line hooked up, and the most common hookup is a Branch (the if in code):

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

chest_device := class(creative_device):

    var Items:[]string = array{"Potion", "Shield", "Key"}

    OnBegin<override>()<suspends>:void =
        # Subscript access goes inside an if: got it, take then; out of bounds, take else
        if (First := Items[0]):
            Print("First slot: {First}")
        if (Nothing := Items[99]):
            Print("Never gets here")
        else:
            Print("Slot 99 does not exist, the grab failed — but the game did not crash")
        # .Length head-counts are always safe, no failure context needed
        Print("Chest holds {Items.Length} items")
        # The idiom for the last slot: its index is Length - 1
        if (Last := Items[Items.Length - 1]):
            Print("Last slot: {Last}")
        # Changing what's in a slot needs a failure context too
        if (set Items[0] = "Big Potion"):
            Print("Slot 0 swapped to Big Potion")

Key points, one by one:

▢ Reaching into an empty slot = a line that doesn't go through, not a crash. The Items[99] line won't blow up the server; it merely makes this Branch's condition come out false, so execution follows the "nothing there" line (the else in code — the equivalent of the False pin). No Blueprint-style Accessed None error, no mid-run crash.

▢ Skip the "nothing there" line, and Compile lights up red. If you don't hook up a Branch and just try to use the result of Elem := Items[0] directly, hit Ctrl+Shift+B to compile and Verse throws you this lesson's most classic red text, which boils down to: "this action might come up empty, and you haven't told me which line to take when it does." Wire it into a Branch (if) and you're fine.

▢ Counting (.Length) never comes up empty. Asking "how many in this row?" is always safe, so Items.Length can go anywhere (the Length node on Blueprint arrays never errors either). It's also the key to grabbing the last item: since numbering starts at 0, the last slot's number is always Length - 1, while reaching into Items[Items.Length] is guaranteed to come up empty — you counted exactly one slot too far, the single most common rookie crash site.

▢ Swapping what's in a slot can also fail to go in. set Items[0] = "Big Potion" is like wiring a Set node to slot 0 and writing in the new value — but this step also has to go through a Branch: the swap only succeeds if slot 0 exists; otherwise the line doesn't go through. Note especially: writing to Items[Items.Length] does not grow the array by one slot — it just doesn't go through. Want to add to the end of the row? Read on.

3. Concatenation with + and a Loop Refresher: The Row Gets Replaced, Never Edited

First, a worldview to install — and this one differs from Blueprint, so watch out: Verse arrays are values; they cannot be edited in place. The Add / Remove / Insert nodes you know from Blueprint — the ones that add or delete an item right inside the array — none of them exist in Verse. Here, every so-called "array modification" actually builds a new row and replaces the entire old row in the variable (think of it as wiring a Set node to the whole array variable). Even set Items[0] = "Big Potion" works this way: under the hood it's "copy the old row, replace slot 0, assign the whole row back."

So "adding someone to the row" relies on concatenation: + joins two rows end to end into a new one; the idiom for adding one item to the tail is set Items += array{NewElement} (read it as "stitch this onto the tail, then assign the whole row back"). Processing every item one by one is Blueprint's ForEach Loop: for (Item : Items) is like wiring the array into a ForEach, with the Loop Body handing you one item per lap; for (Index -> Item : Items) hands you the Array Index too. Hit "Run next step" and watch all these moves in one pass:

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

loot_bag_device := class(creative_device):

    var Loot:[]string = array{"Potion", "Shield"}

    OnBegin<override>()<suspends>:void =
        set Loot += array{"Gold Coin"}
        Print("Bag holds {Loot.Length} items")
        for (Index -> Item : Loot):
            Print("Slot {Index}: {Item}")
        if (Ghost := Loot[3]):
            Print("Never gets here")
        else:
            Print("Slot 3 is thin air, the grab failed")
Output Log

Hit "Run next step" to watch the code execute line by line.

One more advanced treat: Verse's ForEach (for) doesn't just run its laps — each lap's result is automatically collected into a new row. Doubled := for (N : Numbers) { N * 2 } means "double every number and collect them into a new row"; it can filter on the way through too — Big := for (N : Numbers, N > 2) { N } keeps only those greater than 2. One line that does both "process each one" and "filter by condition" — you'll reach for it constantly once you start building your own utility nodes.

4. Slice: Cutting a Stretch Out of the Row

Sometimes you want a stretch, not a single item: the leaderboard's top three, or everyone left after the head of the line departs. Verse builds a Slice tool into arrays: Items.Slice[StartIndex, StopIndex] cuts out the elements from StartIndex up to the slot before StopIndexinclusive head, exclusive tail (the slot you name at the end is not included). The rule is simple: the cut only succeeds when 0 <= StartIndex <= StopIndex <= the item count; otherwise the line doesn't go through.

Mind that the call syntax uses square brackets. Slice can fail to cut, just like grabbing a slot, so in Verse any tool that "might not succeed" is called with square brackets; write parentheses like Items.Slice(0, 2) and one press of Compile lights up red. The brackets are themselves a reminder: "this one might come up empty — remember to hook up a Branch."

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

slice_device := class(creative_device):

    Queue:[]string = array{"One", "Two", "Three", "Four"}

    OnBegin<override>()<suspends>:void =
        # Cut out slots 0 and 1 (not 2): inclusive head, exclusive tail
        if (FrontTwo := Queue.Slice[0, 2]):
            Print("Front row: {FrontTwo.Length} people")
        # Drop the head of the line, keep everyone else
        if (Rest := Queue.Slice[1, Queue.Length]):
            Print("{Rest.Length} people left after the head departs")
        # An out-of-bounds cut just fails, it does not crash
        if (Bad := Queue.Slice[2, 99]):
            Print("Never gets here")
        else:
            Print("No cut: 99 is past the array length")

Two high-frequency moves to memorize: Slice[1, Items.Length] is "drop the first item, keep the rest"; Slice[0, Items.Length - 1] is "drop the last item, keep the rest". Combined with += concatenation from the previous section, you now hold every part needed to build stacks and queues even with "arrays you can't edit in place" — the further reading at the end of this page has the full field exercise.

5. Common Pitfalls: The Minesweeping Manual

Nine out of ten pitfalls in this lesson come from carrying old habits straight over from Blueprint or elsewhere — above all, assuming arrays can be Add-ed / Remove-d in place like Blueprint's. Run yourself through the table:

The trap What happens The right move
Elem := Items[0] used without hooking up a Branch Compile lights up red: roughly "this might come up empty and you gave it no way out" Wire it into a Branch: if (Elem := Items[0]):
Items[Items.Length] Never grabs anything — you counted exactly one slot too far The last slot is Items[Items.Length - 1]
set Items[5] = X without a Branch Same red Compile (this step might not succeed) if (set Items[5] = X):
Writing to Items[Items.Length], hoping it grows a slot like Blueprint's Add Nothing gets added; the line just doesn't go through Add things with concatenation: set Items += array{X}
Items.Slice(0, 2) called with parentheses Compile lights up red Tools that might come up empty take square brackets: Items.Slice[0, 2]
Pre-filling with [1, 2, 3] or array[1, 2, 3] Compile lights up red Keyword + curly braces: array{1, 2, 3}
Hunting everywhere for Blueprint-style Add / Remove / in-place Sort nodes Verse arrays have none of them Arrays are values, no in-place edits; every "modification" produces a new row
set-ing the array mid-ForEach, expecting this lap to change ForEach walks the row captured the moment it started; edits do nothing to it Concatenations/changes made inside the loop only show up on the next ForEach

Watching isn't training. The drill rig below has three of this lesson's keywords carved out: the keyword for pre-filling an array, the property for counting, and the operator for stitching onto the tail. Fill them back in and hit "Check answers":

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

drill_device := class(creative_device):

    # What is the keyword for an array literal?
    var Scores:[]int = ____{90, 80, 70}

    OnBegin<override>()<suspends>:void =
        # Last slot's index = total count - 1
        if (Last := Scores[Scores.____ - 1]):
            Print("Bottom score: {Last}")
        # Append a 60: the concatenation operator
        set Scores ____ array{60}
        Print("Now {Scores.Length} scores in total")

Blueprint Cross-Reference

The array is the container you know best from Blueprint — and the one that differs most. The first two columns line up almost one to one; the column actually worth reading is "Difference".

What you do in Blueprint How you write it in Verse Difference
In the variables panel, flip the container icon beside the type to the nine-square grid → Array, then pick an element type Weapons:[]string = array{"Pickaxe", "Shotgun"} Blueprint switches containers through a UI dropdown; Verse writes the container into the type text: brackets first, element type after. Pre-filling accepts only the array{…} curly braces, and every element must be the same type
Add / Add Unique / Append nodes, stuffing an element straight into the array set Items += array{X} (+ concatenates) Blueprint's Add is an in-place edit; Verse has no Add — += means "build a new row, assign the whole row back". Insert / Remove Index / in-place Sort have no counterparts either
Get (a copy) node: hand it an Index and it spits out the element if (X := Arr[I]): (must sit in a failure context) The critical one this lesson. Out of bounds, Blueprint returns that type's default value (0 / empty string / None) and leaves one warning line in the log while execution carries on — the error is passed downstream in silence; in Verse the subscript itself "might fail", so skipping the Branch makes Compile go red, and reaching too far merely takes the else — no crash, and no fake value handed to you
Length node / Last Index node Arr.Length; the last slot is always Arr.Length - 1 Neither side can fail, so both go anywhere. But Verse has no Last Index counterpart — subtract one yourself; Arr[Arr.Length] can never grab anything
ForEach Loop node with its Array Element and Array Index pins for (Index -> Item : Arr): Nearly identical in shape. The difference: Verse's for also collects each lap's result into a new row, and it accepts a filter condition right in the loop head
An Array variable is a reference: pass it into a function or assign it elsewhere and it is still the same data — change one, both change A Verse array is a value: assigning it hands you an independent copy "Who touched my array?" is a classic Blueprint debugging session; in Verse the question can't arise — including mid-ForEach, where set-ing the source array leaves this lap walking the row captured when it started

Nearly all of these differences grow from one root: a Blueprint array is a container that nodes rewrite in place, while a Verse array is an immutable value. Once the container can't be edited in place, the whole Add / Insert / Remove row of nodes loses its footing, and "concatenate + slice + assign the whole row back" takes over. The price is that every "modification" conceptually builds a new row; the payoff is that no piece of code can ever change, behind your back, the row someone else is holding.

The second root is that "couldn't get it" is written into the type system. Blueprint chose "return a default on out-of-bounds, log a warning" and let the program keep running — the same design instinct behind Accessed None: the error happens at runtime, and usually blows up far from where it started. Verse moves that moment forward to compile time: the subscript action carries a "might fail" marker, and if you don't wire up the failing line it won't compile. The Is Valid-style check you skipped is no longer "good luck" — it's "does not build".

6. Level Challenge

Three mini-levels to check your loot from this lesson. Zero penalty for wrong answers — retry as often as you like.

In Event BeginPlay you write Item := Items[0] and try to use it without hooking up a Branch. What happens?

Items holds 3 items and you wire a Branch to grab slot 3 (if (X := Items[3])), taking A on success and B on failure — what happens next?

An int-holding array var Numbers:[]int = array{1, 2} — you want to add a 3 at the tail. Which is correct?

Further Reading

Technique · EXTRA

Giving Arrays Map / Filter / Reduce

Verse arrays ship without ready-made tools like Map / Filter / Reduce; a community open-source module shows you how to add them yourself — and how to pass a function around like a building block.

Open the extra →

Deep Dive · EXTRA

Why Out-of-Bounds Doesn't Throw

What's really behind the nothing-there case: Verse dry-runs the wiring on a shadow graph first, and if any step fails along the way, even values already changed get rolled back wholesale — as if nothing happened.

Open the extra →

Bonus · EXTRA

Building Stacks and Queues with Immutable Arrays

Length, grabs that can come up empty, Slice, and += — the four-piece kit in combat: build the two classic structures, stacks and queues, without push/pop.

Open the extra →