[UE5] Inventory Basics: A Bag You Pick Up From, Use, and Fill

Created: 2026-07-20Last updated: 2026-09-07

Build an inventory in UE5 Blueprint where you pick up and use herbs. Separates Data Asset definitions from Map quantities and covers per-type stack limits, total capacity, removing entries at 0, and UI that updates only on change, all verified with numbers.

Pick up herbs and they grow to nine; the tenth will not fit. Even when the bag is full, using a herb makes room for something else. Let's build that inventory.

An inventory manages "what you have and how many". Building item descriptions, quantity changes, and display all at once makes it hard to see where to fix things. Start by separating a herb's properties from how many herbs you hold.

Separating an item's definition from the bag's per-type quantities

What You'll Learn

  • Separating item definitions from per-player quantities
  • Respecting per-type stack limits and total bag capacity when changing counts
  • Using one herb and healing the Character's HP
  • Updating a simple inventory display only when quantities change

This hands-on is for people who can use Blueprint variables, functions, and Branches. It combines Data Asset, Map, and Actor Component. We use single-player in the Third Person template, with key input standing in for "pick up".

Sponsored

Separate a herb's properties from how many you hold

Whether you hold three herbs or nine, using one heals the same amount. That shared "what a herb is" information is the definition , stored in a Data Asset.

"I currently hold three" is that player's held state . The bag manages that side.

What is separatedHerb exampleWhere it lives
DefinitionDisplay name Herb, heal 20, stack limit 9DA_Herb
Held stateHolding three DA_HerbThe Inventory Component's Items
One herb definition shared while each player holds their own quantity

Separating them means adjusting the heal from 20 to 25 is one edit in the definition. Keeping a copy of the heal amount per held item would mean updating those too. Our held data holds a reference to the definition and a count .

Here, one count per type

A Map holds pairs of "a key to look up by" and "the matching value". Here the item definition is the key and the quantity is the value. Pass DA_Herb and you get the herb count.

Holding per-type totals with a Map versus holding individual slots with an array
The bag you wantThe easier shape
Only per-type totals such as nine herbs and one keyOur Map
Herbs split into a "nine" slot and a "one" slotAn array of slots
The same sword distinguished at durability 72 and 13An array of structs holding per-item data

A stack holds several of the same type together. With our Map, herbs cap at nine total and the tenth does not go to another slot. And capacity 15 is not "15 types" or "15 slots"; it is 15 items across all types . Decide that rule before building.

Preparation: four definitions and a bag

1. Create the item definition

Use a Third Person Blueprint project. Choose None where a Variant is offered and proceed with the standard BP_ThirdPersonCharacter.

Right-click in the Content Browser and, from "Blueprint Class → All Classes", create BPDA_ItemDefinition with Primary Data Asset as its parent. Add these variables with Instance Editable enabled and Private disabled, then compile and save.

Variable nameTypeDefault
DisplayNameTextEmpty
MaxStackInteger1
HealAmountFloat0.0

Next, create four assets from "Miscellaneous → Data Asset" choosing BPDA_ItemDefinition. That creates data holding values, not duplicates of the Blueprint class.

Asset nameDisplayNameMaxStackHealAmount
DA_HerbHerb920.0
DA_PotionPotion550.0
DA_KeyKey10.0
DA_BombBomb30.0
Creating four data assets from the same BPDA_ItemDefinition

We display names and counts, so no icon images are needed. The difference between a data type and its data is also explained in the Data Asset article.

2. Create the Component managing quantities

Create BP_InventoryComponent with Actor Component as its parent. A Component is a functional part you add to Actors. The bag itself needs no position, so use Actor Component rather than Scene Component.

What to prepareNameSettings
VariableItemsA Map with BPDA_ItemDefinition Object Reference keys and Integer values. Empty default. Private on
VariableMaxCapacityInteger, default 15. Instance Editable on
Event DispatcherOnInventoryChangedNo inputs

Set Items' type to a BPDA_ItemDefinition Object Reference , switch the container to Map, and set the value type on the right to Integer. Do not choose Class Reference.

The Items Map keyed by definition with counts as values, plus the Component's change entry points

An Event Dispatcher tells registered parties "something changed". Add OnInventoryChanged from Event Dispatchers in "My Blueprint". We build the receiving display later.

Do not rewrite Items from outside; change it through the AddItem and RemoveItem we build next. That gathers limit checks and notifications into those two entry points.

Query the current counts

First create three functions inside the Component. The addition logic and the UI use them to query current values.

GetCount: one type's quantity

Create a function GetCount with an input Item (BPDA_ItemDefinition Object Reference) and output Count (Integer), with Pure enabled.

Create the Map Find from a Get of Items, passing the Item input to Key and Value to the Return Node's Count. We do not use Find's Boolean Return Value here. With no such key, the Integer Value is the default 0.

Passing Items and Item to Find and returning Value as GetCount's Count

GetCount is a Pure function that only inspects values, so callers have no white exec pin. Find is the same.

GetKindCount: how many types you hold

Create GetKindCount with no inputs and output Kinds (Integer), with Pure enabled. Create the Map Length from a Get of Items and connect its output to the Return Node's Kinds.

That is not the total quantity. With nine herbs, five potions, and one key, the Map has three entries, so the type count is three.

GetTotalCount: the bag's total quantity

Create GetTotalCount with no inputs and output Total (Integer), with Pure disabled. Create a local variable Sum (Integer) used only inside the function.

Wire white exec from the function entry → Set Sum = 0 → the Map Values → For Each Loop. Values' Target Map is Items and its array output Values goes to For Each Loop's Array.

Zeroing Sum, taking the list of counts with Values, and adding them in a loop

Wire Loop Body to Set Sum and assign "Get Sum + Array Element". Wire Completed to the Return Node with Sum into Total.

Values extracts a Map's values into an array and has white exec pins . It adds 9, 5, and 1 in turn and returns 15 after the loop finishes. Confirm Sum is reset to 0 each time and that Return is not wired to Loop Body.

Compile and save. The entry points exposing quantities outward are ready.

Sponsored

Adding: confirm it fits before rewriting

Create a function AddItem with inputs Item (BPDA_ItemDefinition Object Reference) and Amount (Integer), and output Success (Boolean). Disable Pure and add a local variable CurrentCount (Integer).

Amount is "how many you want to add now". Our rule is to add only if all of it fits, and add nothing otherwise.

1. Verify the item and the amount

From the function entry, wire to the exec-pin Is Valid with Item into Input Object. Is Not Valid prints "No item specified" with Print Text and continues to a Return Node with Success = false.

From the Is Valid side, go to a Branch with Condition Amount <= 0 . True prints "The amount must be at least 1" and returns Success = false. From False, go to Set CurrentCount, assigning GetCount(Item)'s Count.

Continuing to fetching the current count only after Is Valid and the Amount check pass

Is Valid is the entry checking whether the specified target is usable. Pass through it before reading the definition's MaxStack. Zero and negative values are not accepted as additions or removals and do not change quantities.

Later failure paths also end with Print Text → Return Node (Success = false). A Return Node returns the function's result and ends. Placing one per failure point is fine.

The Print Text nodes in this article all use Print to Screen and Print to Log on, Duration 5, and Key None, so you can check failure reasons on screen and in the log.

2. Check the per-type limit and the total capacity

Place a Branch after Set CurrentCount with Condition Amount > Item's MaxStack - CurrentCount . True prints "Cannot stack any more" and returns false.

Dragging from Item's blue pin and creating Get Max Stack reads that definition's limit. Subtract CurrentCount from it and pass the comparison against Amount to the Branch.

From False, call GetTotalCount (Target = Self) and continue from its white output to the next Branch. This Condition is Amount > MaxCapacity - Total . True prints "The bag is full" and returns false.

Checking the per-type remainder and then the bag's total remainder

"Limit minus current count" gives how many more fit. With eight herbs, the remainder is 9 - 8 = 1 . Adding one passes, but adding two is refused. Total capacity works the same: with 15 held, the remainder is 0.

The comparison is > . Do not use >= , which would also refuse adding one into a remainder of one.

3. Add to the current count and write it back to the Map

From the second Branch's False, wire the Map Add → Call OnInventoryChanged → Return Node with Success true.

Add's inputs are Target Map = Items, Key = Item, and Value = CurrentCount + Amount .

Passing the new count to the Map's Add after the limit checks, notifying the change, and returning true

A Map's Add overwrites the value when the key exists. Passing 1 would always leave one. Fetching the current count and writing back the sum lets it grow 1 → 2 → 3.

Removing: delete types that reach 0

Create RemoveItem with the same Item and Amount inputs and Success output, Pure disabled. Create local variables CurrentCount and NewCount as Integers.

The initial Is Valid and Amount <= 0 checks match AddItem, with the same failure messages and false Returns. Once they pass, assign GetCount(Item)'s result to CurrentCount.

The next Branch checks CurrentCount < Amount . True prints "You do not have any" and returns false. From False, Set NewCount = CurrentCount - Amount and check NewCount == 0 in another Branch.

Removing when the result is 0 and writing the new count back with Add when some remain
BranchMap operationWhat follows
True: it reached 0Remove with Target Map = Items and Key = ItemCall OnInventoryChanged → Return (Success = true)
False: some remainAdd with Target Map = Items, Key = Item, Value = NewCountCall OnInventoryChanged → Return (Success = true)

Placing the notification and Return on each branch is fine. We do not use Remove's Boolean return value.

Deleting keys at 0 lets GetKindCount return "how many types you actually hold". Use up your herbs and three types (herb, potion, key) become two (potion, key).

Hands-On: pick up and use a herb

1. Attach the bag to the Character

Open BP_ThirdPersonCharacter, add BP_InventoryComponent from Components' Add, and name it Inventory . Confirm the selected Component's MaxCapacity is 15.

Add CurrentHealth (Float, default 50.0) and MaxHealth (Float, 100.0) to the Character. Also create HerbDefinition (BPDA_ItemDefinition Object Reference) and, after compiling, set its default to DA_Herb.

2. Pick up with key input

Add a keyboard 1 to the event graph. Drag Inventory from Components into the graph to create a reference and build AddItem from it. Wire Pressed to the white exec input with Item = DA_Herb and Amount = 1.

Calling AddItem on the Character's Inventory from the 1 key

Build 2, 3, and 4 the same way. In the Item field, choose the target asset rather than typing text. Success needs no connection at this stage; the Component prints the reason on failure.

KeyItemAmount
1DA_Herb1
2DA_Potion1
3DA_Key1
4DA_Bomb1

Key input is a stand-in for testing pickup. Touching a floor item calls the same AddItem, and you can destroy the floor Actor only when Success is true.

3. Heal only when one was actually removed

Add a keyboard Q to the Character and call the Inventory's RemoveItem from Pressed. Pass a Get of HerbDefinition to Item and 1 to Amount.

Wire the white output to a Branch with Success into Condition. False ends. Only True continues to Set CurrentHealth.

Continuing to the Character's healing logic only when RemoveItem's Success is true

The value to Set is CurrentHealth + HerbDefinition's HealAmount passed through Clamp (Float) with Min 0 and MaxHealth into Max. Clamp keeps a computed result within a range: adding 20 to 90 stops at the maximum 100.

Adding the heal amount to current HP and clamping within maximum HP

After the Set, build HP: {HP} with Format Text, feeding CurrentHealth into HP, and pass it to Print Text.

Our rule consumes the herb even at full HP , so we can watch it count down to zero. To refuse using at full HP, add a check for CurrentHealth < MaxHealth before RemoveItem.

Putting healing on the Character means the Inventory Component never Casts to a specific Character. Attached to a chest, the same add and remove logic works.

Sponsored

Update the display only on change

Finally, add a small UI showing only names and counts. It displays four types in a fixed order, showing 0 for types you do not hold. Before laying out icons, this screen confirms the quantities and the display agree .

We write the display names directly in the UI here, so changing a definition's DisplayName does not change this screen's names.

1. Prepare the display Widget

Create WBP_InventorySummary with User Widget as its parent. Place a Canvas Panel in the Designer and add a Text child named SummaryText with Is Variable enabled. Aim for a top-left anchor with Position (40,40), Size (600,220), and Font Size 22. Do not set a Bind on the Text.

Create InventoryRef (BP_InventoryComponent Object Reference) on the Widget with Instance Editable and Expose on Spawn enabled. Expose on Spawn adds an input for this value on the node creating the Widget.

Passing the Character's Inventory to the Widget so both read the same bag

2. RefreshInventory displays the current values

Create a function RefreshInventory on the Widget with no inputs or outputs and Pure disabled. From the entry, check InventoryRef with the exec-pin Is Valid and continue only on the valid side to GetTotalCount with Target InventoryRef.

Enter this text into Format Text. Use Shift + Enter for line breaks in the field.

Herb: {Herb} / Potion: {Potion}
Key: {Key} / Bomb: {Bomb}
Total: {Total} / Types: {Kinds}

Connect a GetCount targeting InventoryRef to each of Herb, Potion, Key, and Bomb, specifying DA_Herb and the rest for Item. Total is GetTotalCount's output and Kinds is GetKindCount's (Target = InventoryRef).

Wire GetTotalCount's white output to SetText (Text), with a Get of SummaryText into Target and Format Text's Result into In Text.

Turning the four counts and the total and type counts into text and setting it on SummaryText

3. Receive change notifications and display the first value

After compiling, in the event graph go from Construct to Is Valid (InventoryRef) and, on the valid side, call Bind Event to OnInventoryChanged with Target InventoryRef.

Create a Create Event from the red Event input with Object = Self and RefreshInventory selected in the function picker. That specifies what to call when a notification arrives. From Bind's white output, call RefreshInventory (Target = Self) once.

Separating registering the receiver, the initial display, and post-change updates

Bind registers the receiver. Registering does not deliver past changes, so call the initial display yourself. After that, AddItem and RemoveItem update it each time they notify.

From Destruct, also pass through Is Valid (InventoryRef) and, when valid, call Unbind Event from OnInventoryChanged. Pass the same InventoryRef and the same Create Event with Self / RefreshInventory. It ends an unneeded subscription after the display is removed; do not use Unbind All, which would remove other subscriptions.

4. Add it to the screen from the Character

Wire Create Widget to BP_ThirdPersonCharacter's BeginPlay with Class = WBP_InventorySummary, Owning Player = Get Player Controller (Player Index = 0), and the Inventory reference from Components into Inventory Ref. Keep any existing BeginPlay logic and append to it.

Wire Create Widget's white output to Add to Viewport with Return Value into Target. Now the bag the character uses and the bag the screen reads are the same.

There is no UI Tick rebuilding things. Even as you elaborate the display, the Component owns the data and the UI reads current values. The Event Dispatcher article and the UMG article also help.

Confirm: a full bag makes room again

Compile and save everything and Play. Click the screen so key input is received, then try this order.

ActionWhat to confirm
Right after startAll four at 0, total 0, types 0
1 nine timesHerb 9, total 9, types 1
1 once more"Cannot stack any more". The counts do not change
2 five times, 3 onceHerb 9, potion 5, key 1, total 15, types 3
4"The bag is full". Bomb stays 0
QHerb 8, total 14, types 3. HP goes 50 → 70
Q twice moreHerb 6, total 12. HP goes 90 → 100
Q six more timesHerb 0, total 6, types 2. HP stays 100
Q once more"You do not have any". Neither counts nor HP change
4Now you can hold bomb 1. Total 7, types 3
Refusing the bomb at total 15, then using up herbs to reach total 6 and fitting the bomb

A bag that was full makes room again as you use things. Getting this far means the limit checks, removal, and display update all use the same current values.

Try the boundaries a little

Stop Play, change the 1 key's Amount to 2, and restart. Four presses give eight herbs and the fifth is refused, leaving eight. Since there is no partial-add rule, it does not reach nine. Set Amount back to 1 afterwards.

You can also test Amount 0 and -1, which change nothing and print the amount check message. Set it back to 1 at the end.

When it does not work

SymptomWhere to check
Herbs are always 1Whether you pass CurrentCount + Amount to Add
The ninth will not fitWhether the remainder comparison uses >
The total is wrongWhether you add each of Values, reset Sum to 0 each time, and return from Completed
Types stays 3 after using all herbsWhether the 0 branch calls the Map's Remove
It does not healWhether HerbDefinition is DA_Herb, and whether you branch on RemoveItem's Success
The UI is empty from the startCreate Widget's Inventory Ref, the initial Refresh after Construct, SummaryText's Is Variable
Only the UI shows stale countsThe Call OnInventoryChanged after changes, and Bind's Target and Event
Keys do not respondWhether the standard Character is what you control, and whether the game window has input focus

Bonus: extending it to your own game

The same name on a different asset is a different type

The Map's key is a reference to the DA_Herb asset, not the display name. A duplicated DA_Herb2 is a different key even though the screen shows the same "Herb". To test, add one of each with an empty bag and the type count becomes 2.

Do not rely on the Map's own ordering as display order either. Our UI states the order explicitly: herb, potion, key, bomb. As types grow, keep a separate array of definitions defining display order.

When you need per-item data or slots

To keep each sword's durability or the slot position an item sits in, expand to an array of slot structs. When modifying a struct retrieved as a copy, you also need to write it back to the original array. See the struct article.

For a weight system, the idea of checking free space before adding is the same. But you add Weight to the definition, compute totals as "count times weight", and align the limit and comparisons to weight units.

Level changes and the next launch

Our Inventory is attached to the Character, so rebuilding the Character resets its contents. To carry it across level changes, deposit the needed held data in GameInstance.

To keep it for the next launch, write item IDs and counts to Save Game and look the definitions up from the IDs when loading. Recording a reference to a Data Asset and copying the asset's contents into the save are different things. Using IDs also requires logic mapping IDs to definitions.

What we built is a bag whose in-and-out rules live in a Component. Even as you change what triggers pickup or how the UI looks, changing quantities through AddItem and RemoveItem keeps the limit checks and change notifications shared.

Summary

  • Hold an item's "definition" separately from a player's "quantity"
  • Before increasing, check the per-type limit and the bag's capacity
  • Delete types that reach 0 from the Map rather than keeping them
  • Call the display update only when quantities change

The question when building is "is this the same value for everyone, or only for that person?" Same means Data Asset; only theirs means the Component.

Data shapes are covered in Array, Map, and Set and the definition side in Data Asset.

Reference: Map Find, Map Add, Map Values, Binding and unbinding events.

Unreal Engine Notes in this section98