Deep Dive · EXTRA
Why Can Assigning into a map "Fail" Too?
Sit in on a real Epic-forum debate and understand the trade-off behind requiring even "writing a row" to go through a Branch.
Enter Extra →An array finds things by number; a map finds them by key: call out a player's name and the score lands in your hand instantly. This lesson moves the Blueprint Map variable into Verse: how to declare the table, how to look values up by key, how to write and update entries, how to roll-call every row with ForEach — closing with how to choose between Map and Array.
Last lesson's array was a row of numbered lockers: to find anything you call out a number — "what's in locker 3?" But in-game questions usually look like this: "How many points does Bubbles have?" "How many keys is Archie carrying?" What you're holding isn't a number — it's a name. Force an array to do the job and you'd have to rummage through every locker from the front. That work belongs to a map: a registry with keys down the left column and values down the right — call out the key and the value lands in your hand instantly.
To get a Map in Blueprint, you'd flip the container-type dropdown in the variable panel from single value to Map, then pick a type for the Key and one for the Value. Verse has no dropdown — a single line of text does it all: the type inside the square brackets is the key, and the type right behind it is the value. So [string]int reads as "a Map whose keys are strings and whose values are integers" — the same as choosing String for Key and Integer for Value in Blueprint; []int keeps its brackets empty because an Array's "key" is always the position number counting from 0, never yours to fill in. The map{} in the code below pre-fills the table's first few rows, and => is the "this key pairs with this value" arrow — like clicking the + in the default-value panel to add a Key/Value pair:
using { /Fortnite.com/Devices }
registry_device := class(creative_device):
# [string]int: key is the player name, value is the score
var Scores:[string]int = map{"Bubbles" => 10, "Archie" => 25}
# Empty registry: nothing recorded yet
var Kills:[string]int = map{}
Three details worth tapping the blackboard for: first, the pairing arrow is => (equals plus greater-than) — write a colon or -> instead and hitting Compile (that button in Blueprint's top-left; Ctrl+Shift+B in Verse) lights up red just the same; second, for an empty table just write map{} with no rows at all; third, if the same key gets paired twice in the initial contents, the later one wins — one key only ever occupies one row per table, the same reason Blueprint Maps refuse duplicate Keys.
There's also an eligibility check: the key type must be comparable — plainly put, it has to work as a "key" that can be matched against another. int, float, string, char, and enums all qualify out of the box; a regular class does not, unless it carries the <unique> specifier (every instance is born one of a kind, so identity itself can be compared). And player and agent happen to be exactly this kind of unique class, so [player]int is perfectly legal — the official Team Elimination tutorial uses it to track each player's eliminations.
In Blueprint, pulling a value out of a Map is the Find node: it hands you two things at once — the value, plus a "was it found?" boolean. Verse fuses those into one action: Scores["Bubbles"] is that Find, but instead of a separate boolean, "not found" means the whole action doesn't go through. So you wire it into a Branch (that's if) — and this Branch isn't asking True/False, it's asking "did the lookup go through?": if it did, take the main line; if nothing was found, take the else pin. Remember the dice-roll metaphor? if is the gambling table.
Why might it not go through? Because there may be no such player. Elsewhere, a failed lookup might toss you an empty value or even error out and crash the game; Verse's way is much cleaner: the entire lookup doesn't go through, and the execution wire calmly turns into the else pin. You never receive some "empty" value, and nothing crashes — "not found" is locked up tight on the Branch's else side, and it can't leak out.
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
lookup_device := class(creative_device):
var Scores:[string]int = map{"Bubbles" => 10}
OnBegin<override>()<suspends>:void =
if (Score := Scores["Bubbles"]):
Print("Bubbles' score: {Score}")
if (Score := Scores["Mystery"]):
Print("This line never runs")
else:
Print("No such player: Mystery was never registered")
Graph read-through: the skeleton here is an Event BeginPlay (OnBegin, which fires automatically when the game starts). Two Branches hang off it: the first Finds "Bubbles" — found, so it runs Print String with the score; the second Finds "Mystery" — no such entry in the table, doesn't go through, so it turns into the else's Print String and prints "no such player." The Score := half-line stashes the value from the Find in a temporary slot so Print can use it.
Want a default value when a key can't be found? The most straightforward way is that Branch setup above: the main line uses the found value, and else fills in a default. The other road wraps the lookup in option{Scores[Key]}, turning "not found" into an explicit "empty box" instead of a dead end — but that's next lesson's home turf for option, so we'll table it for now.
Writing into the table is, in Blueprint terms, the Map's Add node: give it a Key and a Value. Verse spells it set Scores[Key] = Value — one action, two jobs: key absent, add a row; key present, overwrite the old value — exactly the Add node's "update if present, add if not" temperament. No need to ask "is it there?" first — just write.
But here hides the bit that makes Blueprint veterans stare hardest: this "write a row" action can also fail to go through, so it too must be wired into a Branch. Blueprint's Add node never fails — you connect it and you're done; Verse insists on if (set Scores["Archie"] = 0):, routing the write through a Branch. When you don't need the else side, the community habit is the one-liner if (set Scores["Archie"] = 0) {} — that {} pair is the empty "else does nothing" pin. Skip the Branch and write it bare? Compile lights up red on the spot.
Why can even a "plain write" fail to go through? One-sentence spoiler: because a write like set Scores[K] += 1 — "add onto the current value" — has to read the old value out, do the math, then write it back; when the key doesn't exist, that read step genuinely comes up with no such player. To make every way of "writing a value to some key in a map" obey the same rule, Verse simply requires the most ordinary "direct write" to go through a Branch as well. There's a real official-forum debate behind this — the deep-dive extra page tells the whole story.
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
score_write_device := class(creative_device):
var Scores:[string]int = map{}
OnBegin<override>()<suspends>:void =
# Insert: no "Bubbles" in the table yet, this adds a row
if (set Scores["Bubbles"] = 10):
Print("Registered")
# Overwrite: key exists, 10 becomes 99
if (set Scores["Bubbles"] = 99) {}
# Compound assignment: reads 99 out, writes 100 back
if (set Scores["Bubbles"] += 1) {}
# Danger demo: "Rando" was never registered, the read fails outright
if (set Scores["Rando"] += 1):
Print("This line never runs")
else:
Print("Rando does not exist, += failed")
Graph read-through: four Branches in a row. The first writes "Bubbles => 10" into the empty table (insert) — goes through, Print String says "Registered"; the second changes Bubbles to 99 (overwrite), closed with an empty {} and no log; the third's += 1 reads 99 out and writes 100 back; the fourth tries += 1 on the never-registered "Rando" — the read finds no such player, so it goes straight to else and prints the failure notice.
Two working rules fall out of this: every map write gets wired into a Branch; and before "adding onto the current value," make sure the key was registered with an initial value first — otherwise honestly handle the else side. Now it's hands-on time — the item registry below has three spots carved out: the keyword that opens the table, the pairing arrow, and the write password. Fill them back in and hit "Check Answer."
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
inventory_device := class(creative_device):
# Item registry: item name maps to quantity
var Items:[string]int = ____{"Key" ____ 1}
OnBegin<override>()<suspends>:void =
if (____ Items["Potion"] = 3):
Print("Potion stocked")
To roll-call the whole table, in Blueprint you'd drag a ForEach Loop off the Map: every spin, the Loop Body hands you one Key and one Value. Verse writes it for (Name -> Score : Scores): — the key lands on the arrow's left, the value on its right. Note that this read-out -> looks like the earlier write-in => but works a different shift: => means "this key pairs with this value" when filling the table, -> means "split this pair for me" when looping.
Order matters here: a Verse map roll-calls in insertion order — whoever registered first gets called first; nothing auto-sorts by key, exactly like the paper registry in your head. To count the rows, use Scores.Length, same as an Array's length. One difference from Blueprint to watch out for: Blueprint Maps have a Remove node, Verse maps don't — because like arrays they're "a whole you can't edit in place." To delete a row, the only way is to "rebuild a new table without that key" and swap it in whole; the technique extra page walks you through it hand in hand.
Now let's chain this lesson's moves into one complete scorekeeping device. Hit "Run Next Step" and watch registering, accumulating, and roll-calling happen line by line.
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
score_board_device := class(creative_device):
# Registry: player name -> score
var Scores:[string]int = map{"Bubbles" => 10}
OnBegin<override>()<suspends>:void =
if (set Scores["Archie"] = 0):
Print("Archie registered")
if (set Scores["Archie"] += 5):
Print("Archie +5 points")
for (Name -> Score : Scores):
Print("{Name}: {Score}")
Hit "Run Next Step" to watch the code execute line by line.
The one-sentence rule: data with an inherent order, fetched by position — array; data looked up by identity, one entry per key — map. When unsure, ask yourself: "When I go looking, am I holding a number or a name?"
| Question | array | map |
|---|---|---|
| What you search with | int index (position number counting from 0) | Anything that works as a key (name, ID, player) |
| Variable type / initial contents | Array container of integers; starts with three: 1, 2, 3 | Map container, string keys and integer values; starts with one row: a paired with 1 |
| Adding an element | Add node appends element X at the tail | Add node writes value V for any key K (in Verse it goes through a Branch) |
| Roll call | Drag a ForEach Loop off the Array: elements only, or position numbers included | Drag a ForEach Loop off the Map: every spin gives you one key K and value V pair |
| Typical uses | Spawn order, patrol waypoints, top-ten leaderboards | Player scores, item inventory, ID-to-config lookups |
Now let's line this lesson's most-tripped minefields up in a row and patrol them once:
| Pitfall | What you see | The right move |
|---|---|---|
Writing a bare set M[K] = V without a Branch |
Compile lights up red: this action might not go through and can't stand bare | Wire it into a Branch: if (set M[K] = V) {} — the empty {} is "else does nothing" |
| Assuming a failed lookup hands back an empty / default value | The lookup just doesn't go through (takes else); there is no "empty value" | Supply a default on the Branch's else, or wrap it in option{M[K]} |
| "Adding onto the current value" of a key never given an initial value | The Branch quietly takes else; the score never goes up | Register an initial value first, then accumulate; or honestly handle the else side |
Wrong pairing arrow: a colon as in map{"a": 1}, or "a" -> 1 |
Compile lights up red | There is exactly one pairing symbol: => |
| Using an ordinary Blueprint class as a key | Compile lights up red: keys must be a comparable, key-worthy type | Switch to string / integer / enum; a player is key-worthy by birth |
| Hunting everywhere for a Remove node to delete a key | Verse maps simply have no Remove | The rebuild pattern: filter into a fresh table and refill (see the technique extra page) |
A Blueprint Map and a Verse map hold the same kind of thing, but their temperaments are far apart. The "Difference" column is the real subject of this section.
| What you do in Blueprint | How you write it in Verse | Difference |
|---|---|---|
| Flip the container-type dropdown in the variables panel to Map, then pick a type for Key and one for Value | var Scores:[string]int = map{"Bubbles" => 10} |
Verse says it in one line of text: the key sits inside the brackets, the value right behind them. The key type has to be key-worthy (comparable); a regular class can't be a key unless it carries <unique> — and player and agent happen to be exactly that kind of class |
| Find node: two pins at once, the Value and a "was it found?" boolean | if (V := M[K]): … else: |
Blueprint splits "the value" and "was it found" into two pins, and you may wire only the Value and ignore the boolean — in which case you receive that type's default value. Verse welds the two into one action: not found means the whole line doesn't go through, so you can never obtain a fake value, and you aren't allowed to skip the check |
| Contains node: asks only "is it in there?" and returns a boolean | if (M[K]): (don't catch the value, just see whether it goes through) |
Verse has no separate Contains — the lookup is the existence test, and it hands you the value while it's at it |
| Add node: updates if present, adds if not, never fails — wire it and you're done | if (set M[K] = V) {} |
In Verse even "writing a row" carries the might-not-go-through stamp and must be wired into a Branch; the empty {} is "else does nothing". The reason: a write like set M[K] += 1 has to read the old value first, and with a missing key that read finds no one — so the whole family obeys one rule |
| Remove node / Clear node: gouge a row out in place | No counterpart: filter-rebuild a fresh table and swap it in whole | Same root as arrays — a Verse map is a whole you can't edit in place, so every "modification" is really building a new table |
| Drag a ForEach Loop off the Map; every spin hands you one Key / Value pair | for (K -> V : M):; row count via M.Length |
Same shape, but the order means different things: Blueprint's TMap is a hash table whose iteration order isn't guaranteed and can shift after inserts and removals, so you shouldn't lean on it; a Verse map roll-calls in insertion order, and that order is safe to rely on |
The first two differences are two faces of one thing: Blueprint leaves "was it found?" to your own discipline, Verse welds it into the control flow. Blueprint's Find lets you pull the Value pin and walk away, and the price is that a missing key quietly hands you a default — the score turns into 0, the reference turns into None, and the mistake drifts a long way downstream before it shows itself. Verse simply doesn't offer "a default": not found means this line doesn't go through, so you either handle the else or you don't compile.
The last two differences come from "a map is a value, not a container": with no in-place editing there's no place for Remove to stand, and therefore no "deletions punching holes in hash buckets" either — which is exactly why the order can stay reliably equal to the insertion order. Blueprint's order uncertainty is a side effect of the in-place, hash-table structure.
The registry is drawn up — now clear three mini-challenges to prove it. Wrong answers cost nothing; retry as often as you like.
Right at game start (Event BeginPlay), you write set Scores["Bubbles"] = 10 without wiring it into a Branch. What happens?
Filling in a map's first few rows — which spelling is right?
The Kills table has never registered "Newbie". You now do an "add 1 onto the current value" write on that unregistered key (wired into a Branch) — what happens?
Deep Dive · EXTRA
Sit in on a real Epic-forum debate and understand the trade-off behind requiring even "writing a row" to go through a Branch.
Enter Extra →Technique · EXTRA
How does a table you can't edit in place do "delete"? Regular maps rebuild via filtering; weak_maps reset the whole entry.
Enter Extra →Extension · EXTRA
map's close cousin weak_map is Verse's vehicle for cross-session saves: leaderboards, player progression, and currency systems all start here.
Enter Extra →