What Porting a Simple Inventory System to Verse Taught Me

Published on
6 mins read
Written by

A few weeks ago I sat down to build something dead simple in UEFN: a device that tracks whether a player has picked up a key, and unlocks a door when they have. The kind of thing that's maybe fifteen lines in almost any language I've used. It took me an embarrassingly long afternoon in Verse, not because the logic was hard, but because Verse kept refusing to let me write the code the way my hands wanted to write it.

That friction turned out to be the whole point. Once I stopped fighting it, I actually liked what was on the other side. This is a walkthrough of what surprised me, framed around the device I built, for anyone who's about to open UEFN for the first time coming from C++, JS, or Python.

The first wall: there's no if statement, only if expressions

I started with something like this, muscle memory from every other language:

if (HasKey) {
    OpenDoor()
}

That compiles fine, but I kept trying to write if as a thing that does something, and Verse really wants you to think of it as a thing that produces something. In Verse, everything is an expression, including if. So this is completely legal and idiomatic:

DoorState := if (HasKey) { "Open" } else { "Locked" }

There's no separate "statement" mode: no void-shaped junk-drawer of things that just execute. Once that clicked, I stopped writing awkward temp variables and started writing the value I actually wanted directly.

The wall that actually stopped me for an hour: failure isn't an exception, it's a first-class outcome

Here's where I really got stuck. I wanted to check if the player's inventory array had a key at a given slot, something like:

Item := Inventory[SlotIndex]

In most languages, an out-of-bounds index throws, crashes, or returns null/undefined and you defensively check for it. In Verse, indexing into an array is a failable expression: it either succeeds with a value or fails with none, and that failure isn't something you catch after the fact. It's something the surrounding code has to be a failure context for in the first place.

So the natural way to write my key check ended up looking like this:

KeyDevice := class(creative_device):

    OnBegin<override>()<suspends>:void=
        KeyPickupDevice.PickedUpEvent.Subscribe(OnKeyPickedUp)

    OnKeyPickedUp(Agent:agent):void=
        if (PlayerInventory[Agent] = "key"):
            DoorDevice.Open()

That if isn't testing a boolean I computed earlier: the = comparison itself is failable, and the if is the failure context that catches it. If the lookup or comparison fails, we just silently fall through to whatever's next, no exception object, no try/catch, no null check. It felt uncomfortably implicit the first time, and genuinely elegant by the second.

The bigger payoff showed up when I needed to mutate something conditionally: award an item, then update a quest counter, then only commit both if neither step failed. Functions that can fail this way are marked with the decides effect (and must also be transacts), and Verse calls the resulting behavior speculative execution: if anything in that chain fails, every mutation in it rolls back automatically, as if none of it ran.

TryGrantKeyAndAdvanceQuest(Agent:agent)<decides><transacts>:void=
    PlayerInventory.AddKey[Agent]
    QuestTracker.Advance[Agent]

Coming from languages where I'd normally hand-roll a try/catch/rollback dance around a two-step mutation like this, getting it for free from the type signature was the moment Verse won me over.

Effects are basically a function's resume, and I started reading them that way

Once failure clicked, the rest of Verse's effect specifiers stopped looking like noise. Every function signature is telling you, up front, how much you can trust it:

  • computes: same input, same output, always. Safe to treat like a pure math function.
  • converges: will finish, guaranteed, no infinite loops hiding inside.
  • varies: might not give you the same answer twice; don't cache it.
  • transacts: touches real, mutable state, but can be rolled back on failure.
  • plus decides (can fail) and suspends (can pause and yield control) stacked on top.

I started scanning a function's angle-bracket specifiers before I even looked at its parameters, the same way I'd scan a Rust function for &mut before touching it. It's a small thing, but it changes how fast you can trust unfamiliar code in a shared UEFN project.

Concurrency: I didn't have to think about threads once

My door device also needed to run a short animation while blocking a second interaction from re-triggering it. In most engines that's where I'd start Googling mutexes. In Verse, I just wrote:

OnKeyPickedUp(Agent:agent)<suspends>:void=
    if (PlayerInventory[Agent] = "key"):
        spawn { PlayAnimation() }
        DoorDevice.Unlock()

spawn kicks off that animation as its own async coroutine and moves on immediately, without blocking or any manual thread bookkeeping. If I'd needed the unlock to wait until the animation actually finished, I'd reach for sync or race instead, which are structured concurrency expressions: they keep that concurrent work scoped to the block it started in, so I'm not leaking a background task that outlives the code that spawned it. For gameplay code running across potentially thousands of concurrent player interactions, having this built into the language instead of bolted on as a library was, honestly, a relief.

Classes felt like home, with one weird, good twist

Verse's classes are recognizably OOP: inheritance, public/internal/protected/private, override, final. My door device subclassed creative_device the way you'd expect. The twist was archetypes, Verse's way of instantiating a fully-specified object using a curly-brace literal instead of a constructor call:

tank := class(player_character):
    StartingShields<override>:int = 100
    MaxShields<override>:int = 200

CreateTank():tank = tank{}

It's declarative rather than imperative: you're not running setup code, you're filling in a template of fields. Small thing, but it nudges you toward describing what an object is rather than how it gets built.

Where I landed

My key-and-door device ended up shorter and, weirdly, more honest than the equivalent I've written in other engines: there's no separate error-handling path bolted on afterward, because failure is baked into the same if I was already writing. The effect specifiers took the longest to trust, but now I actually miss them when I'm back in languages that don't have them.

If you're about to try Verse for the first time, my advice: don't fight the failure model, lean into it. Write the "happy path" as an if, and let the language handle the rest.

Try it yourself: open UEFN, drop in a Creative Device, and try building this same key-and-door interaction: it's a great first project because it forces you through the failure/rollback model on day one instead of week two. If you get stuck or want to compare notes, jump into the Fortnite Creative Discord's Verse channels or the UEFN Verse forums: that's where I got unstuck more than once, and it's the fastest way to see how other developers are actually structuring this stuff in production islands.