The Ember Satchel — Build a Custom Inventory
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 ofitem name -> countfor 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:
- The model —
var Satchels : [player][string]int = map{}. The outer map keys byplayer, 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. - 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. - The view — a
hud_message_devicereadout. It repaints ONLY whenSatchelChangedEventfires. The render loop just sleeps onAwait()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:
- Two Trigger devices — one on the lava field (pickup), one at the forge.
- Two Item Granter devices — put a shard-like item (e.g. a consumable) in one and a melee weapon in the other.
- 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
- Add a third item: obsidian ore, from a second pickup plate.
- Change the recipe: a lava blade costs 3 shards and 1 ore. (Hint: call
TrySpendtwice? Careful — what if the first succeeds and the second fails? Better: check both stacks in ONEif, then deduct.) - Make the readout per-player: pass the toucher into
RedrawSatcheland useSatchelBoard.Show(ThatAgent)so each player sees their own counts. - Break-it drill: remove the
Have >= Amountcheck 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]intmap 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 >= Amountinside the failableif) 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
- 01-standalone.verse · standalone
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.
References
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.