The moment you start building an inventory, every decision arrives at once. Where item information lives, how identical items combine, how limits are stored, how the UI is notified. Give the player a single array and start from there, and you'll be rebuilding it every time you add equipment or stacking.
An inventory is assembled from parts that appeared in earlier articles: Data Asset, Map, Struct, Component, Event Dispatcher. This article starts with how to make the design decisions for combining them, and builds up to a minimal inventory where picking up, holding, using, and depleting come full circle.
What You'll Learn
- The design starts with separating the "definition" from the "owned state"
- Owned state goes in either a Map (definition → count) or an array of structs . Two questions decide it
- Split it into an Inventory Component so you can attach it to any Actor
- Where to check the stack limit and the capacity limit
- Notify the UI with
OnInventoryChangedand stop rebuilding it every frame- Hands-on: a minimal inventory where pick up, hold, use, and deplete come full circle
Separate the Definition from the Owned State
There's only one decision to make first. Split item information into two.
| Definition (shared by every copy) | Owned state (specific to that player) | |
|---|---|---|
| Contents | Display name, icon, max stack, heal amount, description | What you have and how many |
| When it changes | Only during development tuning | Constantly during play |
| How many exist | One per item type | One per player |
| Where it lives | Data Asset | A variable on the Inventory Component |

The reason to split is that one definition is enough . The herb healing for 20 is true whether the player holds 3 of them or 99. Copy the heal amount into the owned data and the value at pickup time gets baked in per owner. Rebalance 20 into 25 and every already-collected herb still heals 20.
The owned data only needs which definition it points at (a reference) and how many .
Definitions include references to icons and meshes, so Data Assets fit well. How to create them is covered in the Data Asset article; here we'll just show the shape.
| Variable name | Type | Example (herb) |
|---|---|---|
DisplayName | Text | Herb |
Icon | Texture 2D | (icon image) |
MaxStack | Integer | 9 |
HealAmount | Float | 20.0 |
Note: if the content is mostly numbers and strings and you want to compare 100 rows in a table, a Data Table is fine too. In that case the owned data points at a row name (Name) rather than a definition asset. Everything else in the design stays the same.
Two Ways to Hold What You Own
There are two broad shapes for the owned state. It isn't a question of which is correct, but of which one your game demands.

| Map (definition → count) | Array of structs | |
|---|---|---|
| Shape | Map<ItemDefinition, Integer> | Array<S_ItemSlot> |
| "How many herbs do I have?" | One Find | Scan every element |
| Splitting the same item into two slots | Not possible | Possible |
| Per-instance durability or upgrades | Can't hold them | Can hold them |
| Ordering | Not guaranteed | Guaranteed |
| Games it suits | Materials and consumables | Equipment, crafting, grid-style bags |
Two questions decide it.
- Do identical items differ from each other? If you want to distinguish "iron sword (durability 72)" from "iron sword (durability 13)", a count alone can't express it. Use an array of structs
- Does slot position carry meaning? If "it's in the 9th slot" matters to display or controls, Minecraft-style, only an array guarantees order
If neither applies, a Map is the shortest path . Adjusting counts is two nodes, Find and Add , and "how many do I have" is answered in one step. The hands-on in this article uses a Map.
There's one caveat when you choose a Map. Ordering isn't guaranteed , so you have to decide UI display order separately. Either give the definition an integer like SortOrder and sort the result of Keys , or keep a separate display-order Array.
If you choose an array of structs, you'll inevitably run into the value-type behavior that a struct you fetched is a copy . Get it and reduce durability and the original array doesn't change, so you write it back with Set Array Elem . Details are in the struct article.
Splitting It into an Inventory Component
You could write the inventory directly on the player Character, but making it a Component pays off later . Chests, shop NPCs, enemies you can loot after killing them. Things that "hold contents" show up beyond the player.
The parent class is ActorComponent . It doesn't need a position, so it's not a SceneComponent (→ the Component article).

Here's what goes inside BP_InventoryComponent .
| Kind | Name | Contents |
|---|---|---|
| Variable | Items | Map with BPDA_ItemDefinition (object reference) as key and Integer as value |
| Variable | MaxCapacity | Integer. Total items that can be held. Make it Instance Editable to vary per instance |
| Function | AddItem (Item, Amount) → Success | Entry point : call when something is picked up |
| Function | RemoveItem (Item, Amount) → Success | Entry point : call when something is used or dropped |
| Function | GetCount (Item) → Integer | Returns how many you hold |
| Function | GetTotalCount () → Integer | Returns the total count across all types |
| Event Dispatcher | OnInventoryChanged | Broadcast : announces that the contents changed |
What's exposed outward is only the entry points (functions) and the broadcast (Dispatcher) . The key is never letting anything Set Items directly, which removes any path that modifies the contents without passing the limit checks.
Making MaxCapacity Instance Editable lets you set 15 for the player and 5 for a chest, per placed instance.
Adding: Where to Check the Two Limits
The first thing you build for an inventory is AddItem , and two kinds of limit land here. They're easy to mix up, so let's separate the meanings.
| Limit | Where it's written | What it checks |
|---|---|---|
| Stack limit | The definition ( MaxStack ) | How many of that one item type can stack |
| Capacity limit | The Component ( MaxCapacity ) | How many can be held in the bag overall |
Herbs stack only up to 9, but the bag as a whole holds 15 items. With just one of these, you can't express either restriction fully.

Function: AddItem (inputs: Item / BPDA_ItemDefinition, Amount / Integer → output: Success / Boolean)
→ Is Valid (Input Object: Item)
Is Not Valid → Set Success = false → Return
→ Find (Target: Items, Key: Item)
Value → Promote to local variable (CurrentCount)
→ Branch (Condition: CurrentCount + Amount > Item → Max Stack)
True → Print String ("Can't stack any more of these") → Set Success = false → Return
→ Branch (Condition: GetTotalCount + Amount > MaxCapacity)
True → Print String ("Your bag is full") → Set Success = false → Return
→ Add (Target: Items, Key: Item, Value: CurrentCount + Amount)
→ Call OnInventoryChanged
→ Set Success = true
Three things worth noting.
First, Find returns 0 when nothing is found. Integer's default is 0, so you don't need a branch for "is this the first one". CurrentCount + Amount is already correct even for the very first item.
Second, Map's Add overwrites. Add with an existing key replaces the value rather than adding an element. Find the current count, add to it, and put it back with Add is the standard three-step move for incrementing counts in a Map (→ the containers article).
Third, do the checks before adding. If you Add first and then check whether you went over, you need logic to give the excess back. Refusing before letting it in leaves no room for the state to break.
GetTotalCount gets the array of counts with Values and sums them in a For Each Loop.
Function: GetTotalCount (output: Total / Integer)
→ Values (Target: Items)
→ For Each Loop
Loop Body → Set Total = Total + Array Element
Completed → Return (Total)
Leave this function non-Pure . Making it Pure means it recalculates once for every connection from its output pin. With a loop inside, that count becomes impossible to predict.
Removing and Using
RemoveItem is the reverse of adding, plus one extra move: delete keys that hit 0 .

Function: RemoveItem (inputs: Item / BPDA_ItemDefinition, Amount / Integer → output: Success / Boolean)
→ Find (Target: Items, Key: Item)
Value → CurrentCount
→ Branch (Condition: CurrentCount < Amount)
True → Set Success = false → Return <- don't have it, or not enough
→ Set NewCount = CurrentCount - Amount
→ Branch (Condition: NewCount <= 0)
True → Remove (Target: Items, Key: Item) <- don't leave keys with 0
False → Add (Target: Items, Key: Item, Value: NewCount)
→ Call OnInventoryChanged
→ Set Success = true
You Remove keys that reach 0 so that Length can be used straightforwardly as "the number of item types you hold" . Leave zero-count keys around and the UI keeps showing empty slots, giving you "it's in the list even though I don't have any".
"Using" sits as a thin layer on top of RemoveItem .
Function: UseItem (input: Item / BPDA_ItemDefinition)
→ RemoveItem (Item: Item, Amount: 1)
Success → Branch
True → apply the effect (e.g. heal by HealAmount)
False → Print String ("You don't have one")
Do it in that order: remove first, and only apply the effect on success . The other way around tends to produce a build where you heal even at 0 items. The removal doubles as the sole "do I have one" check, so you never write the condition twice.
The effects themselves, healing or equipping, aren't the inventory's job. The HP side is split out into the health and damage article.
Getting Changes to the UI
The most common mistake in inventory UI is rebuilding the slots every frame on Event Tick . Even when nothing changed, it keeps creating and destroying Widgets every frame.

OnInventoryChanged exists for exactly this. It reaches the UI only at the moment the contents change .
WBP_Inventory
Event Construct
→ Get Owning Player Pawn
→ Get Component by Class (Component Class: BP_InventoryComponent)
→ Promote to variable (InventoryRef)
→ Bind Event to On Inventory Changed (Target: InventoryRef)
Event pin ← Custom Event: RefreshSlots
→ RefreshSlots <- call it once yourself the first time
Custom Event: RefreshSlots
→ Clear Children (Target: SlotContainer)
→ Keys (Target: Items) → For Each Loop
Loop Body →
Create Widget (WBP_ItemSlot)
→ Set Slot Data (Definition: Array Element, Count: GetCount(Array Element))
→ Add Child (Target: SlotContainer)
Event Destruct
→ Unbind Event from On Inventory Changed (Target: InventoryRef)
Two notes.
Call it once yourself in Construct. The Dispatcher only fires "when something changes", so items already held when the UI opens won't show up unless you go read them yourself.
Always Unbind in Destruct. If the Widget disappears first while the subscription remains, notifications get sent to a Widget that no longer exists. Why Bind and Unbind come in pairs is covered in the Event Dispatcher article.
Slot Widget layout and icon display belong to the UMG article. Here it's enough to know the shape of Add Child into a Uniform Grid Panel or Wrap Box .
Hands-On: Closing the Pick Up, Hold, Use Loop
RPG belongings, a roguelike backpack, survival crafting materials, escape-room keys. The loop of pick up, count, use, deplete takes the same shape in every genre. Here we'll skip polished visuals and close the loop while watching the counts through Print String.
What it looks like running
You can pick up 9 herbs but the 10th is refused, the bag fills at 15 items, using one decreases the count, and a type that hits 0 disappears from the list.

Setup to follow along
Create a new project from the Third Person template .
The item definition type (create it following the Data Asset article; parent class is Primary Data Asset )
| Class name | Variable | Type | Notes |
|---|---|---|---|
BPDA_ItemDefinition | DisplayName | Text | Turn on Instance Editable |
MaxStack | Integer | Default 1 | |
HealAmount | Float | Default 0.0 |
Four item data assets (right-click in the Content Browser → Miscellaneous > Data Asset → choose BPDA_ItemDefinition )
| Asset | DisplayName | MaxStack | HealAmount |
|---|---|---|---|
DA_Herb | Herb | 9 | 20.0 |
DA_Potion | Potion | 5 | 50.0 |
DA_Key | Key | 1 | 0.0 |
DA_Bomb | Bomb | 3 | 0.0 |
BP_InventoryComponent (parent class: ActorComponent)
| Variable name | Type | Container | Default value |
|---|---|---|---|
Items | BPDA_ItemDefinition (object reference) → Integer | Map | (empty) |
MaxCapacity | Integer | Single | 15 (Instance Editable) |
Create the functions AddItem / RemoveItem / UseItem / GetCount / GetTotalCount from the previous sections, plus the OnInventoryChanged Event Dispatcher.
BP_ThirdPersonCharacter
| What to prepare | Details |
|---|---|
| Component | Add BP_InventoryComponent ( MaxCapacity set to 15 ) |
Variable CurrentHealth | Float / default 50.0 |
Variable MaxHealth | Float / default 100.0 |
Wiring up the pickup side
To keep verification easy, we'll pick items up with key presses. Using raw keyboard events avoids touching project settings.

BP_ThirdPersonCharacter (event graph)
Keyboard Event: 1 (Pressed)
→ AddItem (Target: BP_InventoryComponent, Item: DA_Herb, Amount: 1)
Success → Branch
True → Print String (Append: "Herb: " + GetCount(DA_Herb) as string)
Keyboard Event: 2 (Pressed) → AddItem (Item: DA_Potion, Amount: 1) <- same shape
Keyboard Event: 3 (Pressed) → AddItem (Item: DA_Key, Amount: 1)
Keyboard Event: 4 (Pressed) → AddItem (Item: DA_Bomb, Amount: 1)
Keyboard Event: Q (Pressed)
→ UseItem (Target: BP_InventoryComponent, Item: DA_Herb)
Keyboard Event: E (Pressed)
→ Print String (Append: "Total: " + GetTotalCount + " / Types: " + Length(Items))
The effect side of UseItem raises the Character's CurrentHealth .
Applying the UseItem effect (inside BP_InventoryComponent)
→ Get Owner → Cast To BP_ThirdPersonCharacter
→ Set Current Health
= Clamp (Value: CurrentHealth + Item → Heal Amount, Min: 0.0, Max: MaxHealth)
→ Print String (Append: "HP: " + CurrentHealth)
Careful: this does a
Cast To BP_ThirdPersonCharacterfrom inside the Component. It works, but that Component is now player-only . If you also plan to attach it to chests, apply the effect on the Owner side instead and have the Component merely broadcast "this was used" (→ the Component article). It's fine to build the working version first and separate them later.
Verifying it
Play and press the keys in this order.
| Action | Expected output |
|---|---|
1 nine times | Counts up from Herb: 1 to Herb: 9 |
1 once more (the 10th) | Can't stack any more of these . Herbs stay at 9 |
2 five times | Potion: 1 through Potion: 5 |
3 once | Key: 1 |
E | Total: 15 / Types: 3 |
4 (bomb) | Your bag is full . 15 + 1 exceeds MaxCapacity |
Q | HP: 70 , herbs drop to Herb: 8 |
Q twice more | HP: 90 → HP: 100 (never 110) |
Q the remaining six times (nine total) | Herbs reach 0, and the 10th Q prints You don't have one |
E | Total: 6 / Types: 2 . The herb key is gone from the Map |
4 (bomb) | Now it goes through. The total is 6, so there's room |
The last two rows are what this hands-on is really about. A bag that was full accepts items again by exactly as much as you used. That's proof the limit check compares against the current total rather than a fixed number.
Troubleshooting if it doesn't work.
- Herbs stay at 1 forever → you're passing 1 straight into
Addwithout adding the result ofFind - The 10th one goes in anyway → you've mixed up where
>=versus>belongs in theMaxStackcheck.CurrentCount + Amount > MaxStackis the correct form Types: 3stays and herbs never disappear →RemoveItemwrites 0 back withAddinstead of callingRemoveat zeroTotalnever goes down →GetTotalCountis countingKeysinstead ofValues- You can pick up even when full →
MaxCapacityis Instance Editable and the placed instance still has the default 0 or an inflated value - The UI doesn't update → you forgot to Call
OnInventoryChangedat the end ofAddItem/RemoveItem. The symptom is correct Print String numbers with stale UI - Nothing happens when you press keys → the Character isn't the possessed pawn. Check the GameMode's
Default Pawn Class(→ the Print String and logging article)
Two things to take away.
- Finish every limit check before letting anything in:
AddItemeither refuses or adds and broadcasts, nothing in between. Never building a path that partially adds and then rolls back removes any chance for the counts to break. Even as entry points multiply, dropping, selling, consuming in crafting, the roads stay the same two:AddItemandRemoveItem - Keep owned data to a definition reference and a count: the display name and heal amount are all in the definition asset. Not copying them into the owned side means rebalancing is nothing more than editing numbers in a Data Asset
Next comes keeping these belongings for the next launch. A reference to a Data Asset can't be saved directly , so give the definition an ID, write out an array of IDs, and look them back up on load. For the steps, continue to the save/load article; for slot visuals and drag operations, continue to the UMG article.
Bonus: Good to Know for Later
- If you place pickups in the world: for items lying on the ground, a single
BP_ItemPickupholding an Instance Editable reference toBPDA_ItemDefinitionis enough. Swapping the Data Asset turns it into an herb or a key, so you never need one Blueprint per item type. If you only want to pick up things within reach, the approach in the Line Trace article applies directly - A weight-based system is the same shape: make
MaxCapacitya weight rather than a count, give the definition aWeight, and changeGetTotalCountinto "total weight". Keeping the check in one place means this kind of spec change is a single-function edit - To carry belongings across levels: a Component attached to the Character is rebuilt on a level transition. To carry things between stages, hand the owned data to the Game Instance, or write it out and read it back around the transition
- Careful when using Data Assets as keys: Map keys must be hashable types. Object references work, but a second asset representing the same item becomes a different key. Don't duplicate
DA_HerbintoDA_Herb2and let both circulate - Equipment belongs in a separate mechanism: "the currently equipped weapon" is part of your belongings, but it has a different rule: only one can be selected . Keeping it apart from the inventory Map as a standalone
EquippedWeaponvariable is more natural. Whether moving an item to equipment also removes it from the inventory is a per-game decision
Summary
- The design starts with separating the definition (Data Asset) from the owned state (a Component variable) . Don't copy definition values into the owned side
- How you hold owned data comes down to do items differ individually, and does order matter . If neither, a Map (definition → count) is the shortest path
- Split the inventory into an ActorComponent . Expose only the entry points (functions) and the broadcast (Dispatcher)
- There are two limits. The stack limit lives in the definition, the capacity limit in the Component. Finish all checks before adding
- The shape for incrementing in a Map is
Find→ add →Add.Removekeys that hit 0 instead of leaving them - Update the UI through
OnInventoryChanged. Call it once yourself in Construct and Unbind in Destruct
In the game you're building, do the things a player carries differ from one another? That answer alone decides between a Map and an array of structs.