【Unity】Building an Inventory System in Unity: ScriptableObject Items, Slots, Stacking & Grid UI

Created: 2026-07-12Last updated: 2026-07-13

Build an RPG or survival inventory by separating data from UI. Define items with a ScriptableObject, design the slot data structure, handle add/remove/stack, reflect it in a grid UI, and save — the foundation of an extensible inventory system.

An RPG's belongings, a survival game's carried items, a crafting game's materials — an inventory is a system essential to many games. Yet when you try to build one, defining items, managing counts, stacking, and updating the UI tangle together and you're unsure where to start. Here too the key is separating data from UI. Split "what and how many you hold (data)" from "how to arrange and show it (UI)," and it becomes surprisingly clean.

This article assembles the foundation of an extensible inventory. Define items with a ScriptableObject, design the slot data structure, handle add/remove/stack, reflect it in a grid UI, and save. An RPG's inventory, a survival backpack, a roguelike's belongings — all built from this design.

The idea of an inventory system: on the left an item master (ScriptableObject), in the middle a list of slots (item + count), on the right a grid inventory UI, with data reflected into the UI

What you'll learn

  • Defining items with a ScriptableObject (item master)
  • The inventory data structure (a list of slots)
  • Add / remove / stack handling
  • Reflecting into a grid UI (Layout Group)
  • Practice: an inventory that picks up, uses, and arranges

Tested on: Unity 2022.3 LTS / Unity 6

Sponsored

Separate data from UI

The first thing to decide when building an inventory is to clearly separate "the belongings data" from "the display UI." Mix them up and you end up editing UI code every time you pick up a single item.

Inventory architecture: item master (ScriptableObject: name, icon, max stack) → inventory data (list of slots: item reference + count) → grid UI (a row of slot UIs), a one-way flow from data to UI

The structure splits into three layers.

LayerRole
Item master (ScriptableObject)The item's "blueprint." Name, icon, max stack — one per type
Inventory data (list of slots)What the player "holds and how many." A collection of item references + counts
Grid UI (Canvas)Reads data and arranges icons and counts in a grid

The important thing is a one-way flow: redraw the UI when data changes. Operations like "pick up" and "use" change only the data, and the UI just redraws based on it. This separation pays off later when you add stacking and reordering.

Define items with a ScriptableObject

First, define the item's "blueprint" with a ScriptableObject. Make one asset per item type, like "Potion" or "Iron Sword."

Item master: ItemData (ScriptableObject) has fields like item name, icon image, max stack, description. Assets line up per type — potion, sword, ore, etc.
using UnityEngine;

[CreateAssetMenu(fileName = "NewItem", menuName = "Inventory/Item Data")]
public class ItemData : ScriptableObject
{
    public string itemName;         // Item name
    public Sprite icon;             // Icon image
    [TextArea] public string description;
    public int maxStack = 99;       // Max count stackable in one slot
}

With [CreateAssetMenu] you can create items from Create > Inventory > Item Data. Give per-type traits to the asset: maxStack = 99 for a potion, maxStack = 1 (unstackable) for a sword. This ItemData is the item type itself — "master data" shared across all players and all drops. The player only holds this reference + count.

The slot list and stacking

The player's belongings are represented by a list of slots. Each slot holds "which item, and how many."

Slots and stacking: the inventory is a list of slots, each holding an item reference + count. When adding the same item, if an existing slot has room, increase the count (stack); otherwise put it in a new slot — a branch
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class InventorySlot
{
    public ItemData item;  // Which item (reference to the type)
    public int count;      // How many held
}

public class Inventory : MonoBehaviour
{
    public List<InventorySlot> slots = new();
    public int maxSlots = 20; // Maximum number of inventory slots

    // Add an item (with stacking)
    // Returns: how many didn't fit (0 means everything fit)
    public int AddItem(ItemData item, int amount = 1)
    {
        // 1. Fill existing slots of the same item, but only "as much as fits"
        foreach (var slot in slots)
        {
            if (slot.item == item && slot.count < item.maxStack)
            {
                int space = item.maxStack - slot.count;   // Room left in this slot
                int moved = Mathf.Min(space, amount);     // Move only the smaller of room vs amount
                slot.count += moved;
                amount -= moved;
                if (amount == 0) break;
            }
        }

        // 2. If any remains, open new slots (as long as slots are available)
        while (amount > 0 && slots.Count < maxSlots)
        {
            int moved = Mathf.Min(item.maxStack, amount);
            slots.Add(new InventorySlot { item = item, count = moved });
            amount -= moved;
        }

        RefreshUI();      // Data changed, so update the UI
        return amount;    // Non-zero means "couldn't carry it all"
    }
}

The key is the stack check. When adding an item, if a slot of the same type already exists, increase its count only by the available room, and spill the remainder into the next slot. Skip the "only as much as fits" part and write slot.count += amount, and adding 20 to a slot with room for just 9 more (90/99) leaves it at 110—blowing straight past maxStack. The one extra step of Mathf.Min(room, amount) prevents this classic bug.

The second trick is returning the count that didn't fit. A non-zero return value means the inventory was full, so the caller can show "Inventory full!" or leave the unclaimed items on the field. Removal is the reverse: decrease the count, and remove the whole slot when it hits 0. Being comfortable with arrays and List makes this smooth.

Sponsored

Reflect into a grid UI

Once you have the data, arrange it in a grid UI. Here uGUI's Layout Group shines.

Grid UI: under a parent with a Grid Layout Group, slot UIs (icon + count text) auto-arrange in a grid. Generate/update as many slot UIs as the inventory data's slots

Placing slot UIs (icon image + count text) under a parent with a Grid Layout Group component auto-arranges them in a grid. Cell size and spacing are also set in the component — no manual coordinate math needed.

UI updates happen in the earlier RefreshUI(). The flow is simple.

  1. Look at each of the inventory's slots
  2. Set the slot UI's item.icon (icon) and count
  3. Clear the icon on empty slots

Just "read data and redraw the UI." Whether data grows or shrinks, calling RefreshUI() syncs the display. The trick is keeping the relationship UI is a mirror of the data.

Practice: pick up, use, arrange

Combining the above, let's complete an inventory that grows when you pick up and shrinks when you use. An RPG's belongings, a survival backpack, a roguelike's items — all the same foundation.

The finished practice: picking up a potion in the field adds it to an inventory slot, and the grid UI shows the icon and count "×3." Using the potion decreases the count, and the slot disappears at 0 — a completed inventory

The overall flow:

  1. Touch a field item (holding ItemData) → call inventory.AddItem(itemData). A non-zero return value means the inventory is full, so show "Can't carry any more!"
  2. AddItem checks stacking, updates data, and redraws the grid with RefreshUI()
  3. Click a slot UI → inventory.UseItem(slot) triggers the effect and decreases the count by 1
  4. When the count hits 0, remove the slot and redraw
// Use an item (decrease count, remove at 0)
public void UseItem(InventorySlot slot)
{
    // Trigger the effect here (heal, equip, etc.)
    ApplyEffect(slot.item);

    slot.count--;
    if (slot.count <= 0) slots.Remove(slot); // Remove the slot when used up
    RefreshUI();
}

Two points: "operations change only data" (pick up, use, drop only rewrite slots; don't touch the UI) and "call RefreshUI on every change" (always redraw the UI after changing data; this habit prevents display desync). Equipment, currency, and crafting materials can be extended with the same mechanism by adding a type (enum) or extra parameters to ItemData. Saving the contents with save & load carries them over on the next launch.

Good to know first

  • Save by "ID," not "reference": the ItemData (ScriptableObject) itself can't be saved to JSON. On save, give each item a unique ID (string) and save "ID and count." On load, look the ItemData back up from the ID. See save & load for details.
  • Drag-and-drop reordering: swapping slots is just swapping slots elements and calling RefreshUI(). Implement UI dragging with interfaces like IBeginDragHandler.
  • Color-code by item type: give ItemData a type (weapon, consumable, material) and change the slot UI's border color — it instantly feels like game UI.
  • Weight and count limits: capping the slot count or limiting by total item weight creates survival-style tension. Both are implemented with checks on the data side.

Summary

  • An inventory splits into three layers: item master (ScriptableObject), data (list of slots), and UI (grid)
  • Define items with a ScriptableObject per type. Give traits like max stack to the asset
  • Belongings are a list of slots (item reference + count). Add "only as much as fits into existing room → spill the rest into new slots," and return what didn't fit
  • The UI auto-arranges with Grid Layout Group. Read data and redraw with RefreshUI()
  • Operations change only data, and redraw the UI on every change. The UI is a mirror of the data

Start by displaying a small inventory of just "holding 3 potions" with a list of slots and a grid UI. Build it to grow on pickup and shrink on use, and you already have a proper belongings system. What treasures will your game's player stuff into their bag?

Further reading