AR
AgentRuss
ECS inventory and equipment architecture showing pickups, inventory data, equipment slots, item definitions, quickbar actions, and stat modifiers.
AgentRuss Guide

How to Design an Inventory and Equipment System in ECS

Written by
AgentRuss
Published

Inventory systems seem simple until they start interacting with the rest of the game.

At first, all we need is:

  • pick up an item
  • store it in inventory
  • show it in the UI

Then we add:

  • equipping weapons
  • armor slots
  • consumables
  • item quantities
  • quickbar shortcuts
  • stat modifiers
  • loot drops
  • item details
  • cooldowns
  • vendors
  • crafting

Before long, inventory touches almost every major gameplay system.

This is exactly where clear ECS boundaries become useful.

Instead of building one giant InventorySystem that owns everything, we can separate:

  • runtime inventory state
  • equipment state
  • static item definitions
  • pickup logic
  • item-use requests
  • item-use resolution
  • stat calculation
  • quickbar state
  • UI presentation

In this guide, we'll build an inventory and equipment architecture that keeps those responsibilities separate while still allowing them to work together.


The Main Pieces

A practical ECS inventory feature might contain:

  • Inventory
  • Equipment
  • Quickbar
  • Pickup
  • ItemDatabase
  • InventorySystem
  • EquipmentSystem
  • ItemUseRequestSystem
  • ItemUseSystem
  • StatsSystem
  • inventory UI systems
  • gameplay events

Each piece has a different purpose.

The first important distinction is between runtime state and item definitions.


Inventory Is Runtime State

An Inventory component describes what a particular entity currently owns.

For example:

JavaScript
1export class Inventory {
2 constructor() {
3 this.items = new Map()
4 }
5}

The map might contain:

  • healthPotion → 3
  • bronzeSword → 1
  • coin → 27

This is runtime state.

Different entities can have different inventories.

The player's inventory may contain twenty items.

A chest may contain three.

An NPC might have none.


Keep Behaviour Out of Inventory

It is tempting to write:

JavaScript
1export class Inventory {
2 constructor() {
3 this.items = new Map()
4 }
5
6 addItem(itemId, amount) {
7 // ...
8 }
9
10 useItem(itemId) {
11 // ...
12 }
13
14 equipItem(itemId) {
15 // ...
16 }
17
18 dropItem(itemId) {
19 // ...
20 }
21}

That begins moving gameplay behaviour into the component.

Soon Inventory needs to understand:

  • item definitions
  • equipment
  • health
  • stats
  • quickbars
  • events
  • UI
  • crafting

A cleaner ECS approach keeps Inventory focused on state while systems own the rules.


Item Definitions Are Static Data

The inventory should not contain the full definition of every item.

Instead, store reusable item information separately.

For example:

JavaScript
1export const ItemDatabase = {
2 healthPotion: {
3 name: 'Health Potion',
4 type: 'heal',
5 amount: 25,
6 },
7
8 bronzeSword: {
9 name: 'Bronze Sword',
10 type: 'equipment',
11 slot: 'weapon',
12
13 modifiers: {
14 attack: 5,
15 },
16 },
17
18 leatherArmor: {
19 name: 'Leather Armor',
20 type: 'equipment',
21 slot: 'body',
22
23 modifiers: {
24 defense: 3,
25 },
26 },
27}

The database answers:

What kind of item is bronzeSword?

The inventory answers:

How many Bronze Swords does this entity currently own?

Those are different responsibilities.


Store IDs Instead of Duplicating Definitions

Suppose the player owns a Bronze Sword.

The inventory does not need to copy:

  • its name
  • attack bonus
  • equipment slot
  • description

It can simply store the ID:

JavaScript
1inventory.items.set(
2 'bronzeSword',
3 1,
4)

Whenever a system needs the definition:

JavaScript
1const item =
2 ItemDatabase['bronzeSword']

This keeps static configuration in one place.

If the sword changes from +5 Attack to +6 Attack, we change the item definition rather than every inventory record.


A Simple Inventory Quantity Model

A useful inventory representation is:

JavaScript
1Map<itemId, amount>

For example:

JavaScript
1inventory.items.set(
2 'healthPotion',
3 3,
4)
5
6inventory.items.set(
7 'coin',
8 27,
9)

To increase an existing quantity:

JavaScript
1const current =
2 inventory.items.get(
3 itemId,
4 ) ?? 0
5
6inventory.items.set(
7 itemId,
8 current + amount,
9)

This works well for stackable items.


Not Every Game Uses the Same Inventory Model

Some games need individual item instances rather than simple quantities.

For example, two swords may have different:

  • durability
  • enchantments
  • random rolls
  • upgrades
  • ownership IDs

In that case, each item instance may need its own runtime identity.

But for items where every instance is identical, storing an item ID and amount is much simpler.

Choose the model based on the game you are building.


Pickups Can Be Components Too

An item lying in the world can be represented with a Pickup component.

For example:

JavaScript
1export class Pickup {
2 constructor(
3 itemId,
4 amount = 1,
5 ) {
6 this.itemId = itemId
7 this.amount = amount
8 this.collected = false
9 }
10}

The entity itself may also have:

  • Transform
  • Mesh
  • collision data
  • Interactable

The Pickup component tells gameplay systems what the entity represents as loot.


Picking Up an Item

A pickup flow might look like:

  1. player interacts with a pickup
  2. interaction resolves
  3. pickup event is created
  4. InventorySystem receives the event
  5. inventory quantity increases
  6. pickup entity is removed or marked collected
  7. UI reflects the new inventory

The important part is that the pickup does not modify the inventory itself.

The pickup stores data.

The inventory system owns the inventory change.


InventorySystem Can Own Inventory Changes

A simple InventorySystem might consume pickup events.

For example:

JavaScript
1const pickups =
2 this.world.events.consume(
3 EVT_PICKUP,
4 )
5
6for (const event of pickups) {
7 const inventory =
8 this.world.getComponent(
9 event.entity,
10 Inventory,
11 )
12
13 if (!inventory) {
14 continue
15 }
16
17 const current =
18 inventory.items.get(
19 event.itemId,
20 ) ?? 0
21
22 inventory.items.set(
23 event.itemId,
24 current + event.amount,
25 )
26}

This gives us one place responsible for adding picked-up items.


Why Use a Pickup Event?

Without an event, the interaction system might directly modify inventory.

That would mean the interaction system needs to understand inventory rules.

Instead, it can communicate:

This entity picked up this item.

Then InventorySystem handles the inventory-specific behaviour.

That keeps interaction and inventory responsibilities separate.


Equipment Is Different From Inventory

Owning an item and equipping an item are not the same thing.

The player might own:

  • three swords
  • two helmets
  • five potions

but only one sword can be equipped in the weapon slot.

That is why equipment deserves its own component.

For example:

JavaScript
1export class Equipment {
2 constructor() {
3 this.weapon = null
4 this.head = null
5 this.body = null
6 this.accessory = null
7 }
8}

The inventory answers:

What do I own?

Equipment answers:

What am I currently using?

Equipment Slots Are Data

A simple RPG might use:

JavaScript
1export const EQUIP_SLOTS = [
2 'weapon',
3 'head',
4 'body',
5 'accessory',
6]

Later, the game could add:

  • gloves
  • boots
  • necklace
  • ring
  • offhand

The exact slots are game-specific.

The architectural idea remains the same.


Item Definitions Can Declare Their Slot

A weapon definition might contain:

JavaScript
1bronzeSword: {
2 name: 'Bronze Sword',
3 type: 'equipment',
4 slot: 'weapon',
5
6 modifiers: {
7 attack: 5,
8 },
9}

Armor might contain:

JavaScript
1leatherArmor: {
2 name: 'Leather Armor',
3 type: 'equipment',
4 slot: 'body',
5
6 modifiers: {
7 defense: 3,
8 },
9}

Now the equipment logic can be data-driven.

It does not need a hard-coded condition for every item.


Equipping Is a Gameplay Rule

Do not make the UI directly write:

JavaScript
1equipment.weapon =
2 'bronzeSword'

just because the user clicked Equip.

The UI should request the action.

A gameplay system should validate and apply it.

Why?

Because equipping may eventually require checking:

  • does the player own the item?
  • is it actually equipment?
  • which slot does it use?
  • is the character allowed to equip it?
  • are level requirements satisfied?
  • is the entity dead?
  • is equipment locked during combat?
  • should an old item be returned somewhere?

Those are gameplay rules.

They do not belong in presentation code.


Request, Validate, Resolve

A useful equipment flow is:

UI or input
requests an equip action.

EquipmentSystem
validates the request.

Equipment component
stores the successful result.

StatsSystem
recalculates effective stats.

UI
reads the updated state.

This follows the same request-and-resolution pattern we use elsewhere in ECS.


An Equip Request Event

For example:

JavaScript
1this.world.events.emit(
2 EVT_EQUIP_ITEM_REQUEST,
3 {
4 entity: player,
5 itemId: 'bronzeSword',
6 },
7)

An EquipmentSystem can consume it.

The source of the request does not matter.

It could come from:

  • inventory UI
  • keyboard input
  • controller input
  • a script
  • eventually a network message

The gameplay system receives the same intent.


Validating Equipment

A simplified equipment system might do:

JavaScript
1const inventory =
2 this.world.getComponent(
3 entity,
4 Inventory,
5 )
6
7const equipment =
8 this.world.getComponent(
9 entity,
10 Equipment,
11 )
12
13const item =
14 ItemDatabase[itemId]
15
16if (!inventory) {
17 continue
18}
19
20if (!equipment) {
21 continue
22}
23
24if (!item) {
25 continue
26}
27
28if (
29 item.type !== 'equipment'
30) {
31 continue
32}
33
34if (
35 !inventory.items.has(
36 itemId,
37 )
38) {
39 continue
40}
41
42equipment[item.slot] =
43 itemId

This is intentionally simple.

The system is the authority deciding whether the equip action is valid.


Equipment Does Not Need to Remove the Item From Inventory

There are several valid inventory models.

One approach is:

Equipped items remain represented in inventory.

The equipment component simply references the item ID.

Another approach removes equipped items from normal inventory slots.

Both can work.

The important thing is to choose a clear rule and keep it consistent.

For a simple ECS RPG, retaining ownership in Inventory and storing the equipped reference separately is often easy to reason about.


Equipment Feeds Into Stats

Now our inventory architecture connects directly to the stats system.

Suppose the player has:

JavaScript
1equipment.weapon =
2 'bronzeSword'

StatsSystem can look up:

JavaScript
1ItemDatabase[
2 equipment.weapon
3]

and find:

JavaScript
1modifiers: {
2 attack: 5,
3}

Then calculate:

  • Base Attack: 5
  • Bronze Sword: +5
  • Final Attack: 10

Equipment stores what is equipped.

StatsSystem decides what those items do to effective stats.


Don't Let Equipment Directly Modify FinalStats

Avoid:

JavaScript
1equipment.equip(
2 'bronzeSword',
3)
4
5finalStats.attack += 5

inside the same component or UI action.

That reintroduces the problems of manually adding and subtracting modifiers.

Instead:

  1. update Equipment
  2. recalculate FinalStats from BaseStats
  3. apply current equipment modifiers

This keeps the stat pipeline deterministic.


Unequipping Becomes Straightforward

To unequip:

JavaScript
1equipment.weapon = null

Then StatsSystem recalculates.

Because final stats begin from base values, the sword bonus disappears naturally.

We do not need to remember:

JavaScript
1finalStats.attack -= 5

This is a major advantage of separating equipment state from derived stats.


Item Use Is Different From Equipment

Not every item is equipped.

A health potion might be consumed.

For example:

JavaScript
1healthPotion: {
2 name: 'Health Potion',
3 type: 'heal',
4 amount: 25,
5}

Using it should not go through the same logic as equipping a sword.

That is why an ItemUseSystem can own consumable behaviour.


Separate Item Use Requests From Item Use Resolution

A useful pipeline is:

Input / Quickbar / Inventory UI
→ item-use request
ItemUseSystem
→ validate inventory
→ consume item
→ emit gameplay effect

This keeps input and UI separate from gameplay.

For example:

JavaScript
1this.world.events.emit(
2 EVT_USE_ITEM_REQUEST,
3 {
4 entity: player,
5 itemId: 'healthPotion',
6 },
7)

Then ItemUseSystem decides what happens.


A Simple ItemUseSystem

A simplified implementation might contain:

JavaScript
1const requests =
2 this.world.events.consume(
3 EVT_USE_ITEM_REQUEST,
4 )
5
6for (const request of requests) {
7 const inventory =
8 this.world.getComponent(
9 request.entity,
10 Inventory,
11 )
12
13 if (!inventory) {
14 continue
15 }
16
17 const amount =
18 inventory.items.get(
19 request.itemId,
20 ) ?? 0
21
22 if (amount <= 0) {
23 continue
24 }
25
26 const item =
27 ItemDatabase[
28 request.itemId
29 ]
30
31 if (!item) {
32 continue
33 }
34
35 if (item.type === 'heal') {
36 inventory.items.set(
37 request.itemId,
38 amount - 1,
39 )
40
41 this.world.events.emit(
42 EVT_HEAL,
43 {
44 entity:
45 request.entity,
46 amount:
47 item.amount,
48 },
49 )
50 }
51}

The exact implementation can vary, but the responsibilities are clear.


Healing Belongs in HealSystem

Even after ItemUseSystem determines that a potion should heal, it does not necessarily need to modify Health directly.

It can emit a heal event.

Then:

JavaScript
1export class HealSystem
2 extends System {
3 update() {
4 const events =
5 this.world.events.consume(
6 EVT_HEAL,
7 )
8
9 for (const event of events) {
10 const health =
11 this.world.getComponent(
12 event.entity,
13 Health,
14 )
15
16 if (!health) {
17 continue
18 }
19
20 health.current =
21 Math.min(
22 health.max,
23 health.current
24 + event.amount,
25 )
26 }
27 }
28}

Now healing from:

  • potions
  • spells
  • regeneration
  • checkpoints
  • food

can potentially reuse the same healing pipeline.


Why Not Make the Potion Heal Directly?

Because the item definition should describe the item.

It should not execute gameplay behaviour.

Avoid something like:

JavaScript
1healthPotion: {
2 use(player) {
3 player.health += 25
4 },
5}

That mixes:

  • static configuration
  • gameplay behaviour
  • entity assumptions

A data-driven definition is easier to:

  • serialize
  • inspect
  • balance
  • load from JSON
  • edit later through tools

Quickbar Is Its Own State

A quickbar is not the same thing as inventory.

The inventory answers:

What items do I own?

The quickbar answers:

What actions have I assigned to these shortcut slots?

A component might look like:

JavaScript
1export class Quickbar {
2 constructor(size = 8) {
3 this.slots =
4 new Array(size).fill(null)
5 }
6}

A slot might reference:

  • a consumable item
  • an ability
  • another usable action

This keeps quickbar configuration separate from item ownership.


Quickbar Slots Can Support Different Action Types

A useful quickbar slot could contain:

JavaScript
1{
2 type: 'item',
3 id: 'healthPotion',
4}

or:

JavaScript
1{
2 type: 'ability',
3 id: 'heavyStrike',
4}

Now the same bar can support both consumables and abilities.

Input determines which slot was pressed.

Gameplay systems determine what that assigned action means.


Quickbar Input Should Produce Intent

If the player presses key 1, the quickbar system should not necessarily heal the player directly.

Instead, it can inspect the assigned slot.

For an item:

JavaScript
1this.world.events.emit(
2 EVT_USE_ITEM_REQUEST,
3 {
4 entity: player,
5 itemId:
6 slot.id,
7 },
8)

For an ability, it might produce an ability request.

The quickbar becomes an input mapping layer rather than the owner of item or ability gameplay.


Inventory UI Should Display State

The inventory UI has important responsibilities:

  • display items
  • show quantities
  • show selected item details
  • show item modifiers
  • show equipped state
  • create user requests

But it should not become the authority on gameplay.

The UI reads:

  • Inventory
  • Equipment
  • ItemDatabase

Then presents that information.

If the user clicks Equip, it requests an equip action.


Keep Inventory Toggle and Inventory Rendering Separate

Even inside UI, responsibilities can be separated.

For example:

InventoryToggleSystem

answers:

Should the panel be open?

InventoryUISystem

answers:

What should the panel display?

Those are different responsibilities.

This becomes useful as the UI grows to include:

  • sorting
  • filters
  • comparisons
  • tooltips
  • crafting
  • vendors
  • controller navigation

Show Equipped State From Equipment

Do not maintain a second independent UI boolean such as:

JavaScript
1item.isEquipped = true

if Equipment already contains the source of truth.

The UI can check:

JavaScript
1equipment.weapon ===
2 itemId

or inspect the other slots.

That prevents gameplay state and UI state from disagreeing.


Item Details Can Read the Definition

When an item is selected, the inventory panel can show information from ItemDatabase.

For example:

Bronze Sword

  • Weapon
  • Attack +5

The UI does not need to understand how the modifier affects final combat calculations.

It only displays the modifier definition.

StatsSystem remains responsible for applying it.


Inventory Quantities Need Clear Rules

When using a consumable, decide what happens when its quantity reaches zero.

For example:

JavaScript
1const nextAmount =
2 amount - 1
3
4if (nextAmount <= 0) {
5 inventory.items.delete(
6 itemId,
7 )
8} else {
9 inventory.items.set(
10 itemId,
11 nextAmount,
12 )
13}

This keeps the inventory clean.

The same helper logic can be centralized inside the relevant inventory system if many features need it.


Don't Let Every System Edit Inventory Arbitrarily

As the game grows, many systems may want to affect inventory:

  • pickups
  • item use
  • crafting
  • vendors
  • quests
  • loot
  • rewards

If all of them directly mutate inventory.items, debugging becomes harder.

A stronger architecture may centralize inventory changes through:

  • InventorySystem
  • inventory commands
  • request events

That gives inventory mutation a clear owner.


Item Removal Can Be a Request Too

For example:

  • consume item
  • sell item
  • craft with item
  • drop item

all require inventory quantities to change.

Eventually you may want requests such as:

  • add item
  • remove item
  • move item
  • use item

Whether all of these need events depends on the project.

Do not create events merely for the sake of it.

Use them when they help establish a meaningful responsibility boundary.


Loot Drops Fit Into the Same Architecture

Suppose an enemy dies and drops a coin.

The death or loot system can create an entity with:

  • Transform
  • Mesh
  • Pickup

For example:

JavaScript
1world.addComponent(
2 pickup,
3 new Pickup(
4 'coin',
5 7,
6 ),
7)

When the player collects it, the existing pickup and inventory pipeline handles the result.

The loot system does not need special code for how inventory stores coins.


Random Loot Amounts Belong Before Inventory Storage

Suppose a drop gives between 3 and 8 coins.

The loot system can determine:

JavaScript
1const amount =
2 randomInt(
3 3,
4 8,
5 )

Then create:

JavaScript
1new Pickup(
2 'coin',
3 amount,
4)

InventorySystem receives the final amount and adds it.

This keeps random loot generation separate from inventory storage rules.


Equipment and Inventory Can Share Item Definitions

One benefit of ItemDatabase is that several systems can use the same data.

Inventory UI
reads names and descriptions.

EquipmentSystem
reads equipment type and slot.

StatsSystem
reads modifiers.

ItemUseSystem
reads consumable type and amount.

Loot UI
reads names and icons.

One definition becomes the shared description of the item.


Don't Put UI-Specific State in ItemDatabase

Static item definitions should describe the game item.

Avoid fields such as:

JavaScript
1selected: true
2inventoryRow: 4
3isHovered: false

Those belong to UI state.

Likewise, runtime quantity does not belong in the definition.

ItemDatabase should not say:

JavaScript
1healthPotion: {
2 quantity: 3
3}

because every inventory can own a different amount.


Static Definition vs Runtime State

A useful distinction is:

ItemDatabase

  • name
  • type
  • icon
  • description
  • modifier values
  • equipment slot

Inventory

  • item ID
  • quantity

Equipment

  • currently equipped item IDs

Quickbar

  • assigned action IDs

Pickup

  • item ID
  • amount
  • collected state

Each type of data has a clear home.


Events Can Connect the Entire Flow

A larger item flow might look like:

World Pickup
EVT_PICKUP
InventorySystem
→ Inventory changes

Then later:

Inventory UI
EVT_EQUIP_ITEM_REQUEST
EquipmentSystem
→ Equipment changes
→ Stats recalculate
EVT_STATS_CHANGED
→ UI refreshes

Or:

Quickbar Input
EVT_USE_ITEM_REQUEST
ItemUseSystem
→ item consumed
EVT_HEAL
HealSystem
→ Health changes

No single system needs to own the entire chain.


System Order Still Matters

If equipment affects combat stats during the same frame, the order should be predictable.

For example:

  1. resolve equipment changes
  2. recalculate stats
  3. resolve combat
  4. update UI

If combat runs before stats recalculate, it could use the previous equipment values.

This is where inventory architecture connects directly to ECS system order.


Same-Frame vs Next-Frame Requests

Not every inventory action needs to complete immediately.

For example, an equip request might be processed later in the same frame.

Or some structural changes may be deferred until the next update.

What matters is consistency.

If input creates a request during one phase, the resolving systems should run in an order that makes the outcome predictable.


Saving Inventory Becomes Straightforward

Data-focused components are easy to serialize.

For example, Inventory can become:

JavaScript
1{
2 healthPotion: 3,
3 coin: 27,
4 bronzeSword: 1,
5}

Equipment:

JavaScript
1{
2 weapon: 'bronzeSword',
3 head: null,
4 body: 'leatherArmor',
5 accessory: null,
6}

Quickbar:

JavaScript
1[
2 {
3 type: 'item',
4 id: 'healthPotion',
5 },
6
7 {
8 type: 'ability',
9 id: 'heavyStrike',
10 },
11]

These are clean pieces of state.


Derived Stats Do Not Need to Be Saved

Because equipment and BaseStats can regenerate FinalStats, a save system may not need to persist the calculated result.

When loading:

  1. restore BaseStats
  2. restore Inventory
  3. restore Equipment
  4. restore relevant modifier sources
  5. recalculate FinalStats

That reduces the chance of stale derived values being saved.


Multiplayer Benefits From Clear Ownership

Inventory is a sensitive gameplay system in multiplayer.

A client should not be able to simply say:

I now have 999 swords.

Instead, an authoritative server can process requests.

For example:

Client

Use potion.

Server

  • does the player own it?
  • can it be used?
  • remove one
  • apply effect
  • replicate result

The request-and-resolution architecture we've already built fits naturally with that model.


Equipment Requests Work the Same Way

A multiplayer equip request might eventually contain:

JavaScript
1{
2 itemId: 'bronzeSword',
3}

The server validates:

  • ownership
  • slot
  • requirements
  • current state

Then updates Equipment.

The client receives the authoritative result.

The UI does not become the authority simply because the player clicked the button.


Item Instances Can Be Added Later

If the game eventually needs unique equipment, the architecture can evolve.

Instead of inventory storing:

JavaScript
1'bronzeSword' -> 1

it may store an item instance ID:

JavaScript
1'item_10482'

That item instance could contain:

  • definition ID: bronzeSword
  • durability: 71
  • rarity: rare
  • bonus attack: 3

Equipment then references the instance rather than only the static definition.

The higher-level separation remains the same.


Stackable and Unique Items Can Coexist

A mature RPG may use:

Stackable item

Health Potion × 8

Unique item

Bronze Sword
Durability 72
Critical +3%

The inventory representation becomes more advanced, but the architecture still separates:

  • definitions
  • instances
  • inventory ownership
  • equipment state
  • gameplay systems

You do not need that complexity before the game requires it.


Crafting Can Build on Inventory

Crafting may eventually request:

  • remove 3 Iron Ore
  • remove 1 Wood
  • add 1 Iron Sword

A CraftingSystem does not need to become the owner of inventory storage.

It can validate recipes and then request inventory changes.

The existing inventory architecture remains useful.


Vendors Can Build on Inventory Too

A vendor transaction may involve:

  • player inventory
  • currency
  • vendor inventory
  • prices

A VendorSystem owns trade rules.

Inventory components continue storing ownership.

This is a good sign that the architecture is composable.

New gameplay features can work with existing state instead of replacing it.


Keep Responsibilities Clear

A useful division is:

Inventory Component

What does this entity own?

Equipment Component

What is currently equipped?

Quickbar Component

What shortcuts are assigned?

Pickup Component

What item exists in the world to collect?

ItemDatabase

What is this item type?

InventorySystem

How does inventory state change?

EquipmentSystem

Can an item be equipped, and where?

ItemUseSystem

Can this item be used, and what gameplay effect should begin?

StatsSystem

How does equipment affect effective stats?

UI Systems

How should all of this state be presented?

That separation keeps one feature from becoming one enormous class.


A Useful Inventory Checklist

When adding an inventory-related feature, ask:

Is this reusable item configuration?

Put it in the item definition.

Is this something an entity currently owns?

Store it in Inventory.

Is this currently equipped?

Store it in Equipment.

Is this assigned to a shortcut?

Store it in Quickbar.

Is this an item in the world?

Use something like Pickup.

Is the player requesting an action?

Create gameplay intent.

Does the action require validation?

Resolve it inside a gameplay system.

Does equipment change stats?

Let StatsSystem recalculate them.

Is this only presentation?

Keep it in the UI layer.


Common Inventory Architecture Mistakes

Letting the UI directly change equipment

The UI should request the action.

Putting item quantities in ItemDatabase

Quantities are runtime inventory state.

Putting gameplay methods inside Inventory

Inventory should remain data-focused.

Modifying FinalStats directly when equipping

Let StatsSystem recalculate from BaseStats.

Hard-coding every item ID inside systems

Prefer item definitions and data-driven rules.

Giving the quickbar its own item quantities

Inventory should remain the ownership source of truth.

Making Pickup modify Inventory directly

Let gameplay systems coordinate the change.


The Bigger Picture

A complete item flow can span several ECS responsibilities.

A sword pickup might move through:

Pickup entity
→ interaction
→ inventory
→ equipment request
→ equipment state
→ StatsSystem
→ FinalStats
→ combat

A potion might move through:

Inventory
→ quickbar
→ use request
→ ItemUseSystem
→ inventory quantity decreases
→ HealSystem
→ Health changes

The systems are separate, but the data flows through a clear pipeline.


Why This Architecture Scales

The biggest benefit is that future features can build on existing boundaries.

We can add:

  • loot rarity
  • durability
  • vendors
  • crafting
  • item comparison
  • equipment requirements
  • two-handed weapons
  • armor sets
  • consumable cooldowns
  • drag and drop
  • controller navigation
  • item instances
  • multiplayer validation

without turning Inventory into a giant object that owns every item-related rule.

Each new responsibility can attach to the existing architecture where it belongs.


Conclusion

A good ECS inventory system is not one system.

It is a collection of focused responsibilities working together.

Inventory stores ownership.

Equipment stores equipped items.

Quickbar stores shortcut assignments.

Pickup describes loot in the world.

ItemDatabase describes reusable item definitions.

InventorySystem handles inventory changes.

EquipmentSystem validates equipment actions.

ItemUseSystem resolves consumables.

StatsSystem converts equipment modifiers into effective stats.

UI systems display the state and produce player requests without becoming gameplay authorities.

That gives us a clean flow from world loot all the way to combat:

Pickup
Inventory
Equipment
Stats
Gameplay

and another flow for consumables:

Quickbar / Inventory UI
Use Request
ItemUseSystem
Gameplay Effect

The key principle is the same one that appears throughout ECS architecture:

Keep state, configuration, gameplay rules, input intent, and presentation in clearly defined places.

Once those boundaries are established, inventory stops being an isolated feature and becomes a reusable foundation for equipment, loot, consumables, crafting, vendors, abilities, saving, and eventually multiplayer.