Add, Remove, Stack, and Broadcast Inventory Changes¶
Inventory bugs usually start when AddItem half-succeeds silently. Make the rules explicit: validate, stack, create slots, report overflow, then broadcast once.
At a glance
Use this for: the first AddItem, RemoveItem, stack, and UI refresh
rules -
Main data: FInventoryEntry structs in an array, optionally backed by
a map from item ID to slot index -
Returns you should design: accepted count, rejected count, and failure
reason -
Official docs: Blueprint Arrays,
Blueprint Maps,
Struct Variables in Blueprints,
and Event Dispatchers
The one-minute version¶
AddItemshould decide whether to add to an existing stack, create a new stack/slot, accept only part of the request, or reject the whole request.RemoveItemshould reduce counts, remove empty stacks, clear equipped state when needed, and report whether it actually removed enough.- Arrays give display order and slot positions. Maps give fast lookup by item ID. Use the simplest shape that still answers the question.
- Duplicate item IDs are normal in arrays when several stacks exist. Duplicate keys are not allowed in maps.
- Invalid item data and non-positive quantities should fail without mutating inventory.
- Broadcast
OnInventoryChangedonce after a successful mutation, not once per widget and not from Tick.
Start with the result contract¶
Do not make AddItem return only "done" if partial success matters. Design a
small result struct:
Struct FInventoryChangeResult
bChanged (Boolean)
AcceptedCount (Integer)
RejectedCount (Integer)
FailureReason (Enum or Text for debug/UI)
Useful reasons:
EInventoryFailureReason
None
InvalidItem
InvalidQuantity
InventoryFull
NotEnoughItems
ItemNotUsable
EquippedItemBlocked
This result makes pickup cleanup safe:
Pickup asks AddItem(Potion, 5)
-> AcceptedCount = 3
-> RejectedCount = 2
-> pickup keeps Quantity = 2 or stays in world
Without this contract, full inventories tend to either delete items or duplicate them.
The data shape¶
A compact first inventory can use one array:
Struct FInventoryEntry
ItemId (Name)
Count (Integer)
SlotIndex (Integer)
Durability (Float, optional)
EquippedSlot (Enum or Name, optional)
InventoryComponent
Entries: Array of FInventoryEntry
MaxSlots: Integer
OnInventoryChanged dispatcher
This is enough for small bags. Every add/remove can loop through Entries to
find a matching item ID.
Add a map only when lookup noise becomes real:
ItemIdToFirstSlot: Map Name -> Integer
Blueprint Maps cannot use duplicate keys. If one item ID can occupy several
stacks, a plain Map Name -> Integer only remembers one slot. That is fine for
"one stack per item ID" designs, but wrong for separate stacks or unique item
instances. For multiple stacks, looping the entries array is often clearer in
Blueprint until a real performance problem appears.
AddItem flow¶
The normal AddItem order is:
- validate item ID/data and quantity;
- find compatible existing stacks;
- fill those stacks up to max stack;
- create new slots/stacks if space remains;
- return accepted/rejected counts;
- broadcast once if anything changed.
pseudocode - Blueprint-friendly, not engine source
function AddItem(ItemId, Quantity):
Result = empty result
if Quantity <= 0:
Result.FailureReason = InvalidQuantity
return Result
Definition = FindItemDefinition(ItemId)
if not found:
Result.FailureReason = InvalidItem
return Result
Remaining = Quantity
MaxStack = Definition.MaxStack
for each Entry in Entries:
if Remaining <= 0:
break
if Entry.ItemId == ItemId and Entry.Count < MaxStack and Entry can stack:
Space = MaxStack - Entry.Count
Move = Min(Space, Remaining)
Entry.Count += Move
Remaining -= Move
Result.AcceptedCount += Move
set array element back
while Remaining > 0 and Entries.Length < MaxSlots:
Move = Min(MaxStack, Remaining)
Add new FInventoryEntry(ItemId, Move, SlotIndex)
Remaining -= Move
Result.AcceptedCount += Move
Result.RejectedCount = Remaining
Result.bChanged = Result.AcceptedCount > 0
if Remaining > 0 and Result.AcceptedCount == 0:
Result.FailureReason = InventoryFull
if Result.bChanged:
RebuildLookupMap if you use one
Call OnInventoryChanged
return Result
The set array element back line matters. Structs are values. If you break an
entry, change Count, and never set the array element to the changed struct, the
stored inventory did not change.
Stack rules¶
Stacking by item ID is correct only for items with no meaningful per-instance state.
Good stackable items:
- small potions;
- ammo;
- coins;
- crafting materials;
- keys only if duplicate keys are meaningful in your game.
Usually not stackable by ID alone:
- weapons with durability;
- items with rolled stats;
- quest items that must stay distinct;
- equipped items;
- items with custom names, charges, upgrades, or bound ownership.
Use definition data to decide:
Definition:
MaxStack = 10
bCanStack = true
Entry:
ItemId = Potion_Small
Count = 7
For unique items:
Entry:
ItemId = Sword_Iron
InstanceId = 3982
Count = 1
Durability = 0.72
If an entry has InstanceId, durability, or upgrades, do not merge it with
another entry just because the item ID matches.
RemoveItem flow¶
RemoveItem is the mirror image of AddItem. Decide whether it removes from a
specific slot or by item ID.
Remove from a specific slot when:
- the player dragged one slot to trash;
- a consumable button is tied to one slot;
- unique item state matters;
- UI order matters.
Remove by item ID when:
- spending currency;
- consuming ammo;
- crafting ingredients;
- paying costs from any matching stack.
function RemoveItem(ItemId, Quantity):
if Quantity <= 0:
return InvalidQuantity
Available = CountItem(ItemId)
if Available < Quantity:
return NotEnoughItems
Remaining = Quantity
for each matching Entry:
Move = Min(Entry.Count, Remaining)
Entry.Count -= Move
Remaining -= Move
if Entry.Count == 0:
if Entry is equipped:
Unequip first or block removal
remove array element
else:
set array element back
RebuildLookupMap if used
Call OnInventoryChanged once
return success
Choose your equipped-item rule up front. A first project usually wants one of these:
| Rule | Behavior |
|---|---|
| Block removal if equipped | Drop/use/delete fails until unequipped. |
| Unequip then remove | Removal clears equipped actor/component first. |
| Remove only extra stacks | Equipped slot keeps its instance; costs come from non-equipped entries. |
Do not silently destroy an equipped actor because some crafting recipe spent the same item ID from the bag.
Empty slots and full inventory¶
There are two common slot models.
Compact list¶
Entries:
[0] Potion x4
[1] Key x1
[2] Ammo x20
Removing Key shifts later entries down. This is simple for small menu lists, but drag/drop slot positions can move unless you handle it.
Fixed slots¶
Slots:
[0] Potion x4
[1] Empty
[2] Ammo x20
[3] Empty
Fixed slots make grid UI and drag/drop clearer. They require an explicit empty
slot representation: either an entry with bIsEmpty, or a separate array of
optional item data.
Inventory full means different things:
- no empty fixed slot;
- no compact-list capacity left;
- existing stacks are full and no new stack can be created;
- weight limit reached;
- item category limit reached.
Return the reason that your UI needs. "Full" is not always enough.
Broadcast once¶
The inventory owner should broadcast after the mutation is complete.
AddItem
-> change Entries
-> rebuild lookup map
-> update equipped state if needed
-> Call OnInventoryChanged(Entries or InventoryComponent reference)
Do not do this:
Widget Tick
-> Get Player Character
-> Get Inventory
-> rebuild all slots every frame
Do not do this either:
AddItem loops 5 stacks
-> Call OnInventoryChanged after every stack touched
That makes UI rebuild several times for one pickup and can expose temporary states in the middle of a mutation.
The pattern:
WBP_Inventory opens
-> get InventoryComponent
-> bind to OnInventoryChanged
-> RefreshInventory once from current Entries
InventoryComponent changes
-> Call OnInventoryChanged once
-> widget RefreshInventory
When the widget closes or changes owner, unbind or rebind according to your normal widget lifetime pattern.
Invalid data and duplicate IDs¶
Invalid item data should not mutate inventory.
Examples:
- item ID not found in Data Table;
- Data Asset reference is
None; - max stack is
0or negative; - quantity is
0or negative; - item category is blocked by game rules;
- entry count is already invalid because of an old bug.
During development, fail loudly:
AddItem invalid ID
-> Print String "AddItem failed: missing item definition Potion_Small"
-> return AcceptedCount 0, RejectedCount Quantity
Duplicate item IDs are normal in arrays:
[0] Potion x10
[1] Potion x3
Duplicate item IDs are not normal in a simple lookup map:
Map ItemIdToSlotIndex:
Potion -> 1
The map lost slot 0. If you need many stacks per ID, use a deliberate helper structure/code lookup that stores every matching slot, or skip the map until the simple array loop is not enough.
Multiplayer boundary¶
Inventory is gameplay state. In multiplayer, make the server own actual mutation.
Client presses pickup/use/drop
-> ServerRequestAddItem / ServerRequestUseItem
Server
-> validates distance, item, quantity, ownership, rules
-> mutates inventory
-> replicates inventory summary or changed entries
Client UI
-> updates from replicated state or RepNotify
Event Dispatchers are local notification tools. They are useful after state changes on a machine, but they do not replace replication. A client-side dispatcher call does not make the server inventory change.
When it fails (and what failure does)¶
| Symptom | Likely cause |
|---|---|
| Pickup deletes item when bag is full | AddItem returned no accepted/rejected result, or pickup ignored it. |
| Count changes in the graph but not in inventory | Struct copy was edited without setting the array element back. |
| Potion stacks past max | AddItem never clamps available stack space. |
| UI refreshes several times per pickup | Broadcast happens inside the stack loop. |
| UI never refreshes on open | Widget bound after old changes and did not do an initial refresh. |
| Removing an item leaves an empty count-zero entry | RemoveItem did not remove or mark empty entries. |
| Using one equipped item destroys another | RemoveItem removed by item ID without respecting equipped instance/slot. |
| Map lookup points to wrong slot | Array changed and lookup map was not rebuilt, or map cannot represent multiple stacks. |
| Multiplayer client sees item but server rejects use | Client mutated local inventory state instead of requesting server authority. |
Failure should look like a result struct, a false Boolean, or a visible debug print while developing. Do not let failure look like "nothing happened, maybe."
What these functions do not do¶
- They do not define item data by themselves.
- They do not create or destroy world pickups unless you call those paths.
- They do not equip actors, attach sockets, or play use effects by themselves.
- They do not save to disk automatically.
- They do not make UI update unless you broadcast or refresh.
- They do not make invalid item IDs safe to ignore.
- They do not replicate client changes by themselves.
Lookalikes - which one do I want?¶
| Tool | Use when | Watch out |
|---|---|---|
| Array of entries | You need ordered inventory slots or a simple small bag. | Check length and set struct elements back after edits. |
| Map ItemId -> count | You only need counts by stable item ID. | It has no slot order or per-instance state. |
| Map ItemId -> slot index | One stack per item ID and fast lookup matters. | Multiple stacks need a different value shape. |
| Set of IDs | You only care whether something is unlocked/owned. | It cannot store counts. |
| Event Dispatcher | Inventory changed and listeners should refresh. | It does not carry authority or replay old changes. |
| Widget binding | Tiny UI value that is cheap and truly derived. | Inventory polling from bindings grows expensive and hides logic. |
| SaveGame copy | Persist durable item IDs/counts to disk. | It is not the live inventory owner. |
Rule of thumb: mutation belongs in one inventory owner; display listens to the owner; saves copy values out and back in.
Going deeper¶
- Small Inventory Architecture - the definition/pickup/entry/equipped split this page assumes.
- Arrays, Sets, Maps, and ForEach - choosing the container shape and avoiding index/key traps.
- Structs, Enums, and Switch on Enum - entry structs, result structs, and failure-reason enums.
- Data Tables vs Data Assets - where item definitions come from.
- Event Dispatchers - one inventory owner broadcasting to zero, one, or many UI/listener objects.
- Updating a Widget: Bindings vs Events - why inventory UI should refresh from events instead of polling.
- Save Game: Create / Save / Load Game to Slot - copying durable entries into and out of save data.
- Replication basics - authoritative server changes and replicated client presentation.
- Official docs: Blueprint Arrays, Blueprint Maps, Struct Variables in Blueprints, and Event Dispatchers.
- Engine source (requires engine access - we do not reproduce it here):
Blueprint container helpers in
UKismetArrayLibrary,UKismetSetLibrary, andUKismetMapLibraryunderEngine/Source/Runtime/Engine/Classes/Kismet/; Blueprint struct nodes such asUK2Node_MakeStruct,UK2Node_BreakStruct, andUK2Node_StructMemberSetunderEngine/Source/Editor/BlueprintGraph/Classes/; dispatcher nodesUK2Node_AddDelegate,UK2Node_AssignDelegate, andUK2Node_CallDelegateunder the same BlueprintGraph tree.