Skip to content

Small Inventory Architecture

A pickup in the world, an inventory entry, an item definition, and an equipped weapon are four different things. Keeping them separate is what makes a first inventory stop collapsing into casts and stale references.

At a glance

Use this for: the first single-player inventory shape - Main pieces: item definition data, world pickup actor, runtime inventory entry, equipped actor/component, UI, and SaveGame data - Beginner owner: usually an InventoryComponent on the player character or a player-owned framework object - Official docs: Data Driven Gameplay Elements, Data Assets, Components, and Save Game

The one-minute version

  • Item definition data says what the item is: display name, icon, max stack, pickup mesh, equipment class, use behavior, value, weight, tags.
  • A world pickup is a live actor in the level that can be overlapped, traced, interacted with, hidden, disabled, destroyed, or respawned.
  • An inventory entry is runtime owned state: item ID or data reference, count, slot, durability, equipped state, and any per-instance values.
  • An equipped actor or component is the live thing attached to the player: weapon actor, shield mesh, held torch, armor component, or ability object.
  • UI should read the inventory owner and definitions. UI should not search the world for pickups or become the authority that changes inventory.
  • SaveGame should store stable IDs and counts, not live actor, component, widget, or pickup references.

The architecture picture

The simplest useful inventory has this shape:

flowchart LR
    Definition[Item definition data<br/>Data Table row or Data Asset]
    Pickup[World pickup actor<br/>BP_Pickup]
    Inventory[Inventory owner<br/>InventoryComponent]
    Entry[Inventory entry struct<br/>ItemId + Count + state]
    UI[Inventory UI]
    Equipped[Equipped actor/component]
    Save[SaveGame data]

    Pickup -->|carries ItemId/count| Definition
    Pickup -->|AddItem request| Inventory
    Inventory --> Entry
    Entry -->|looks up display/equip data| Definition
    Inventory -->|OnInventoryChanged| UI
    Inventory -->|spawn/attach/use/drop| Equipped
    Inventory -->|copy IDs/counts| Save

The important part is direction. Pickups feed the inventory. UI listens to the inventory. SaveGame records durable values. Item definitions are read-only design data. Equipped actors are live world objects created from inventory state, not the inventory state itself.

The four item shapes

Item definition data

Definition data is the shared description of an item.

For a first project, either a Data Table row or a Data Asset can work:

Data Table row: Potion_Small
    DisplayName = Small Potion
    Icon = T_UI_PotionSmall
    MaxStack = 10
    UseType = Heal
    HealAmount = 25

Data Asset: DA_Rifle
    DisplayName = Rifle
    Icon = T_UI_Rifle
    MaxStack = 1
    EquipmentActorClass = BP_Rifle_Equipped
    PickupActorClass = BP_RiflePickup

Definition data is not the item you own. It is the recipe or catalog entry. Several players, pickups, shops, save files, and UI widgets can point at the same definition.

Good definition fields:

  • stable item ID;
  • display name and icon;
  • max stack;
  • item category or tags;
  • equipment slot;
  • actor/component class to spawn when equipped;
  • pickup mesh or pickup actor class;
  • use effect values;
  • weight, value, rarity, sound, and visual effects.

Bad definition fields:

  • current count in one player's bag;
  • reference to a placed pickup actor;
  • current durability for one sword instance;
  • UI widget reference;
  • "is equipped by player right now."

Those are runtime facts, not shared definitions.

World pickup actor

A pickup actor is the thing in the level.

BP_Pickup
    StaticMeshComponent
    CollisionComponent
    ItemId or ItemDefinition
    Quantity

Its job is to be found by overlap or trace, then ask the inventory owner to add the item. After a successful add, it decides what happens to the world actor: destroy, hide and disable collision, start a respawn timer, or leave a partial quantity behind.

Player overlaps pickup
-> get player's InventoryComponent
-> InventoryComponent AddItem(ItemId, Quantity)
-> Branch AddResult.AcceptedCount > 0
   true:
       reduce pickup Quantity or Destroy Actor
   false:
       show "inventory full" feedback

The pickup is not a save slot and not the inventory entry. If the pickup is destroyed after collection, the item can still exist in the inventory because the inventory stored data, not a pointer to that pickup.

Inventory entry

An inventory entry is the runtime record that the player owns something.

Struct FInventoryEntry
    ItemId (Name)
    Count (Integer)
    SlotIndex (Integer)
    Durability (Float, optional)
    EquippedSlot (Enum or Name, optional)

Use a struct so the fields travel together in arrays, functions, save data, and UI refreshes.

For a tiny inventory, this is enough:

InventoryComponent
    Entries: Array of FInventoryEntry

When the project needs faster lookup, add a map:

ItemIdToSlotIndex: Map Name -> Integer

Do not add the map on day one unless you need it. The quick path of "use an array and loop through it" is fine for a small bag; the concrete cost appears only when every add/remove/use graph repeats the same search and the slot list gets large enough to become noisy.

Equipped actor or component

An equipped item is a live gameplay representation created from inventory state.

Examples:

  • BP_Rifle_Equipped actor attached to a hand socket;
  • shield actor attached to the offhand socket;
  • flashlight component enabled on the character;
  • armor component that changes stats;
  • ability object/component activated by the selected entry.

The inventory entry says "the player owns rifle ID 1." The equipped actor is the live rifle model and behavior in the world.

Equip Rifle entry
-> spawn BP_Rifle_Equipped from the rifle definition
-> attach to character mesh socket
-> store EquippedActor reference
-> mark EquippedSlot = Weapon

If attachment or spawn fails, the inventory entry should usually stay in the bag. Do not delete the item before the live equipped representation exists.

Where the inventory should live

For a first single-player project, start with an InventoryComponent.

Owner Good when Watch out
Player Character The character is stable and inventory can reset or be rebuilt on respawn/travel. Death/respawn destroys the component unless you copy state elsewhere.
PlayerController UI/menu ownership and pawn replacement matter more than physical body ownership. PlayerController is local player focused; do not put world pickup behavior here.
PlayerState Multiplayer or respawn-persistent public player state matters. It is extra framework weight for a tiny single-player prototype.
GameInstance You need a temporary cross-map handoff while the app keeps running. It is not saved to disk and should not hold live actor references.
SaveGame You need durable inventory after quitting. It is not a live runtime inventory and does not run gameplay.

The beginner default:

BP_PlayerCharacter
-> InventoryComponent
   Entries
   AddItem
   RemoveItem
   EquipItem
   OnInventoryChanged dispatcher

If the pawn can be destroyed and recreated, choose one of these patterns before the first bug:

Respawn rebuild pattern:
    Character InventoryComponent -> copy plain entries to PlayerController/GameInstance
    Destroy old pawn
    Spawn new pawn
    copy entries into new InventoryComponent

Persistent owner pattern:
    PlayerController or PlayerState owns InventoryComponent
    current pawn asks that owner for inventory actions

For multiplayer, the server should own authoritative add/remove/equip changes. Clients can show UI and request actions, but they should not be trusted to create items, spend ammo, or drop valuable objects by changing local arrays.

What UI should read

UI is a view and command surface.

Good UI jobs:

  • display slots in order;
  • read display name, icon, count, rarity, and equipped state;
  • call RequestEquip, RequestUse, RequestDrop, or dispatcher-style events back to the controller/inventory owner;
  • refresh after OnInventoryChanged;
  • show disabled/full/invalid states.

Bad UI jobs:

  • search the world for pickup actors every frame;
  • mutate Entries directly from a button without going through inventory rules;
  • store the only copy of item counts;
  • save the inventory to disk by reading widget labels;
  • decide server-authoritative gameplay in multiplayer.

The normal flow:

InventoryComponent AddItem succeeds
-> update Entries
-> Call OnInventoryChanged

WBP_Inventory
-> bound event receives changed inventory
-> rebuild visible slots from Entries + item definitions

When the widget is created after the inventory already has items, bind then refresh once manually. Dispatchers do not replay old calls.

What SaveGame should store

SaveGame stores durable inventory facts:

Struct FSavedInventoryEntry
    ItemId (Name)
    Count (Integer)
    SlotIndex (Integer)
    Durability (Float, optional)
    EquippedSlot (Name or Enum, optional)

BP_SaveGame
    SavedInventory: Array of FSavedInventoryEntry

It should not store:

  • the pickup actor you overlapped;
  • the equipped weapon actor attached to your hand;
  • the UI slot widget;
  • the inventory component object reference;
  • current overlap sets;
  • temporary input/menu state;
  • full item definitions that already exist in Data Tables or Data Assets.

On save, copy runtime entries into SaveGame. On load, copy saved entries into the current inventory owner, then rebuild UI and equipped actors in the new world.

Load save
-> Open saved map
-> new player/inventory owner starts
-> copy SavedInventory into InventoryComponent
-> for EquippedSlot entries, spawn/attach live equipment
-> broadcast OnInventoryChanged

Failure patterns

Symptom Likely architecture problem
Pickup disappears but no item appears Pickup destroyed itself before confirming AddItem accepted the item.
UI shows an item but gameplay cannot use it UI owns display state instead of reading the inventory owner.
Item count resets on respawn Inventory lived only on a pawn that was destroyed.
Save/load creates empty or broken items Save stored live actor references instead of stable IDs/counts.
Equipping duplicates weapons Equip spawned a new actor without unequipping or destroying the previous equipped actor.
Stackable items lose durability/custom state The entry stacked by ItemId even though instances should stay separate.
Multiplayer clients disagree about inventory Client mutated local inventory arrays without server authority/replication.
Data row changes do not update owned items The entry copied definition fields once; decide whether owned items read definitions live or snapshot values.

What this architecture does not do

  • It does not require a marketplace inventory plugin.
  • It does not decide your final large-game item database.
  • It does not make Data Tables, Data Assets, or SaveGame run gameplay by themselves.
  • It does not make UI authoritative.
  • It does not make world pickups survive after destruction.
  • It does not solve multiplayer replication automatically.
  • It does not force every item to be an actor. Most items can stay as data until equipped, used, or dropped.

Lookalikes - which one do I want?

Thing Use it for Not for
Item definition Shared authored data: name, icon, max stack, classes, values. Per-player count or durability.
World pickup A live level actor the player can touch or interact with. Long-term owned inventory state.
Inventory entry The player's owned item/count/state. Meshes, collision, sockets, or world overlap events.
Equipped actor/component Live representation in hand/on body/active gameplay. The durable save record.
Inventory UI Display and action requests. Owning item rules or world searches.
SaveGame Durable IDs/counts/equipped slots. Live actor references or current widgets.
GameInstance Temporary travel handoff. Saving after the app closes.

Rule of thumb: definition is what the item is, pickup is where it is in the world, entry is what the player owns, equipped actor is what is currently live.

Going deeper