The Ember Satchel — Build a Custom Inventory
Tutorial beginner compiles

The Ember Satchel — Build a Custom Inventory

Updated beginner Code verified

The Ember Satchel — Build a Custom Inventory

Welcome to the east volcano, creator! Up here on Emberpeak, scavengers collect ember shards from the lava fields and haul them to the forge to smelt a lava blade. Fortnite's built-in inventory can hold the items — but it can't COUNT them for you, can't enforce "3 shards per blade", and can't paint a live readout on screen.

So we'll build our own: the Ember Satchel, a custom inventory managed entirely in Verse. Real devices still hand the player real items — Verse keeps the books.

What You'll Learn

  • How to store per-player item stacks in a [player] map (a bag of item name -> count for every player).
  • How to back your Verse ledger with an item granter device, so players hold the real item.
  • How to build a buy / carry / spend loop: scavenge shards, carry them, spend 3 at the forge.
  • How to keep a HUD readout in sync using an event — no polling loops.

How It Works

Think of the system in three parts:

  1. The modelvar Satchels : [player][string]int = map{}. The outer map keys by player, so every player gets their own bag. The inner map keys by item name ("ember_shard", "lava_blade") and stores a stack count. This map IS the inventory — the single source of truth.
  2. The muscle — two item_granter_devices. When the satchel says "+1 shard", ShardGranter.GrantItem(Toucher) puts the real item in the player's hands. Verse decides, the device delivers.
  3. The view — a hud_message_device readout. It repaints ONLY when SatchelChangedEvent fires. The render loop just sleeps on Await() between changes.

Two triggers drive the loop: a pickup plate on the lava field (+1 shard) and a forge plate (spend 3 shards, gain 1 blade). Spending goes through TrySpend, which checks the stack FIRST — a player with 2 shards walks away with nothing missing.

Let's Build It

In UEFN, place these five devices and name them so you can find them:

  1. Two Trigger devices — one on the lava field (pickup), one at the forge.
  2. Two Item Granter devices — put a shard-like item (e.g. a consumable) in one and a melee weapon in the other.
  3. One HUD Message device — this becomes the satchel readout.

Create a new Verse device, paste the code, build, and drag it into the level. Then wire each @editable slot in the Details panel to the matching device.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

# THE EMBER SATCHEL - a Verse-managed custom inventory.
# The satchel is the MODEL: one bag of item stacks per player.
# The granter devices are the MUSCLE: they put the real item in hand.
# The HUD board is the VIEW: it repaints only when a satchel changes.
ember_satchel_device := class(creative_device):

    # Step on this plate to scavenge an ember shard.
    @editable
    PickupPlate : trigger_device = trigger_device{}

    # Step on this plate at the forge to spend 3 shards on a lava blade.
    @editable
    ForgePlate : trigger_device = trigger_device{}

    # Grants the real shard item, so the pickup feels physical.
    @editable
    ShardGranter : item_granter_device = item_granter_device{}

    # Grants the forged lava blade.
    @editable
    BladeGranter : item_granter_device = item_granter_device{}

    # The satchel readout everyone can see.
    @editable
    SatchelBoard : hud_message_device = hud_message_device{}

    # THE SATCHEL: each player owns a bag; each bag maps item name -> count.
    var Satchels : [player][string]int = map{}

    # One announcement: "a satchel changed." The view sleeps on this.
    SatchelChangedEvent : event() = event(){}

    # hud_message_device text must be a `message`, not a string.
    SatchelText<localizes>(S : string) : message = "{S}"

    OnBegin<override>()<suspends> : void =
        # DisplayTime 0.0 = persistent. The readout never blinks out.
        SatchelBoard.SetDisplayTime(0.0)

        # Gameplay feeds the model...
        PickupPlate.TriggeredEvent.Subscribe(OnPickup)
        ForgePlate.TriggeredEvent.Subscribe(OnForge)

        # ...and the render loop feeds the screen. No polling.
        RedrawSatchel()
        loop:
            SatchelChangedEvent.Await()
            RedrawSatchel()

    # CARRY: +1 ember shard into the toucher's satchel.
    OnPickup(MaybeAgent : ?agent) : void =
        if (Toucher := MaybeAgent?, P := player[Toucher]):
            AddItem(P, "ember_shard", 1)
            # Back the ledger with the real item in their hands.
            ShardGranter.GrantItem(Toucher)

    # SPEND: 3 shards -> 1 lava blade.
    OnForge(MaybeAgent : ?agent) : void =
        if (Smith := MaybeAgent?, P := player[Smith]):
            # Call TrySpend OUTSIDE the failable if - it signals an event,
            # so it can't run inside a condition that might roll back.
            Spent := TrySpend(P, "ember_shard", 3)
            if (Spent?):
                AddItem(P, "lava_blade", 1)
                BladeGranter.GrantItem(Smith)

    # THE ONLY PLACE stacks grow - and one of two places that signal.
    AddItem(P : player, Item : string, Amount : int) : void =
        var Bag : [string]int = map{}
        if (Existing := Satchels[P]):
            set Bag = Existing
        var NewCount : int = Amount
        if (Have := Bag[Item]):
            set NewCount = Have + Amount
        if (set Bag[Item] = NewCount) {}
        if (set Satchels[P] = Bag) {}
        SatchelChangedEvent.Signal()

    # THE ONLY PLACE stacks shrink. Fails softly when the player is short.
    TrySpend(P : player, Item : string, Amount : int) : logic =
        var Spent : logic = false
        if (Bag := Satchels[P], Have := Bag[Item], Have >= Amount):
            var NewBag : [string]int = Bag
            if (set NewBag[Item] = Have - Amount) {}
            if (set Satchels[P] = NewBag) {}
            set Spent = true
            SatchelChangedEvent.Signal()
        Spent

    # Read the WHOLE model, paint the WHOLE board. Never partial updates.
    RedrawSatchel() : void =
        var Shards : int = 0
        var Blades : int = 0
        for (P -> Bag : Satchels):
            if (S := Bag["ember_shard"]):
                set Shards = Shards + S
            if (B := Bag["lava_blade"]):
                set Blades = Blades + B
        SatchelBoard.SetText(SatchelText("EMBER SATCHEL  |  SHARDS {Shards}  |  BLADES {Blades}"))
        SatchelBoard.Show()

Let's look at the important parts.

The nested map. Satchels[P] fails if P has no bag yet — that's why AddItem starts from an empty map{} and only copies the existing bag when the lookup succeeds. Map writes are failable too, so they use the if (set Bag[Item] = NewCount) {} pattern.

One writer per direction. Stacks only ever grow in AddItem and only ever shrink in TrySpend. When a bug appears later, you know exactly where to look — and both places Signal() the change so the view redraws.

Spend runs outside the failable if. TrySpend signals an event, an effect the compiler will not allow inside a condition that could roll back. So OnForge calls it first, stores the logic result in Spent, and only queries that value (if (Spent?)) in the condition.

Spend checks first. TrySpend puts Have >= Amount in the same failable if as the lookups. If any link fails — no bag, no stack, not enough — nothing is deducted and it returns false. The forge simply does nothing for a broke smith.

The granter is the hand, Verse is the brain. GrantItem(Agent) needs an agent, which we already have from the trigger. The device drops the real item; the map keeps the count that game logic trusts.

Try It Yourself

  1. Add a third item: obsidian ore, from a second pickup plate.
  2. Change the recipe: a lava blade costs 3 shards and 1 ore. (Hint: call TrySpend twice? Careful — what if the first succeeds and the second fails? Better: check both stacks in ONE if, then deduct.)
  3. Make the readout per-player: pass the toucher into RedrawSatchel and use SatchelBoard.Show(ThatAgent) so each player sees their own counts.
  4. Break-it drill: remove the Have >= Amount check and watch players forge blades with empty satchels. Put it back. That one comparison is your whole economy.

Recap

You built a working custom inventory! You learned that:

  • A [player][string]int map gives every player a private bag of item stacks.
  • An item granter device backs the Verse ledger with real, holdable items.
  • Spend-checks-first (Have >= Amount inside the failable if) keeps the economy honest.
  • An event + Await render loop keeps the HUD readout fresh without polling.

The Ember Satchel pattern scales to any economy: coins, keys, quest tokens. Same model, same muscle, same view. Keep forging!

References

  • https://dev.epicgames.com/documentation/en-us/fortnite/verse-api/fortnitedotcom/devices/item_granter_device
  • https://dev.epicgames.com/documentation/en-us/fortnite/verse-api/fortnitedotcom/devices/trigger_device
  • https://dev.epicgames.com/documentation/en-us/fortnite/verse-api/fortnitedotcom/devices/hud_message_device
  • https://dev.epicgames.com/documentation/en-us/fortnite/map-in-verse

Verse source files

Check your understanding

Test yourself with an interactive quiz and track your progress + earn XP — free for members.

Turn this into a guided course

Add Working with items, pickups, and inventory in Verse to your free study plan — we'll suggest related pages and stitch the lot into one compile-checked, self-guided lesson with worked examples and quizzes.

Original tutorial generated by Verse Island from the Verse/UEFN knowledge base, with references to the Epic Games sources above. Code is validated against the knowledge base.

Comments

    Sign in to vote, comment, or suggest an edit. Sign in