Skip to main content

Building a Reusable Inventory System

A data-driven inventory system using ScriptableObjects that I can drop into any Unity project with minimal changes.


The Problem

Every game project eventually needs some form of inventory. I'd written three throwaway versions — each tightly coupled to the specific game it was built for. When Sharpshooters needed a weapon + ammo inventory, I decided to build a version that would actually transfer to future projects.

Requirements I set:

  1. Items defined in data, not code — adding a new item should take 5 minutes, not an hour
  2. Stacking — identical items merge up to a configurable max stack
  3. UI-decoupled — inventory logic should work without any UI attached
  4. Serializable — must survive save/load round-trips cleanly

Data Model

Everything starts with ItemData — a ScriptableObject that defines what an item is:

[CreateAssetMenu(fileName = "NewItem", menuName = "Inventory/Item")]
public class ItemData : ScriptableObject
{
public string itemId;
public string displayName;
public Sprite icon;
public int maxStackSize = 1;
public ItemCategory category;

[TextArea] public string description;
}

ItemCategory is an enum: Weapon, Ammo, Consumable, QuestItem.

The actual inventory slot holds a reference to ItemData plus a current quantity:

[Serializable]
public class InventorySlot
{
public ItemData item;
public int quantity;

public bool IsEmpty => item == null;
public bool CanAdd(int amount) => quantity + amount <= item.maxStackSize;
}

Inventory Manager

The InventoryManager is a plain C# class (not a MonoBehaviour) so it can be instantiated, serialized, and tested without a scene:

public class InventoryManager
{
private readonly List<InventorySlot> slots;
public event Action OnInventoryChanged;

public InventoryManager(int capacity)
{
slots = new List<InventorySlot>(capacity);
for (int i = 0; i < capacity; i++)
slots.Add(new InventorySlot());
}

public bool TryAdd(ItemData item, int amount = 1)
{
// First: try to stack onto existing slot
foreach (var slot in slots)
{
if (!slot.IsEmpty && slot.item == item && slot.CanAdd(amount))
{
slot.quantity += amount;
OnInventoryChanged?.Invoke();
return true;
}
}

// Second: find empty slot
var empty = slots.FirstOrDefault(s => s.IsEmpty);
if (empty == null) return false;

empty.item = item;
empty.quantity = amount;
OnInventoryChanged?.Invoke();
return true;
}

public bool TryRemove(ItemData item, int amount = 1)
{
var slot = slots.FirstOrDefault(s => s.item == item && s.quantity >= amount);
if (slot == null) return false;

slot.quantity -= amount;
if (slot.quantity == 0) slot.item = null;
OnInventoryChanged?.Invoke();
return true;
}
}

The OnInventoryChanged event is how UI stays in sync — the UI listens to this event and rebuilds when it fires. Inventory never calls UI directly.


Save / Load

Serializing ScriptableObject references is the tricky part — you can't serialize the asset itself, only a stable ID. ItemData.itemId is that stable ID.

[Serializable]
public class InventorySaveData
{
public List<SlotSaveData> slots;
}

[Serializable]
public class SlotSaveData
{
public string itemId;
public int quantity;
}

On save: serialize itemId + quantity per slot.
On load: look up ItemData by itemId from an ItemRegistry (a ScriptableObject that holds all ItemData assets), then restore the slot.


What Went Well

  • Decoupling paid off immediately. UI changes broke zero inventory logic during development.
  • ScriptableObjects scale gracefully. Going from 5 to 30 item types required zero code changes — just new assets.
  • Events over polling. The UI refresh is instant and doesn't require an Update() loop.

What I'd Change

  • Stack overflow edge case. If TryAdd is called with amount > maxStackSize, it silently fails. Should split into multiple slots or return a partial-add count.
  • No slot ordering. Items land wherever the first empty slot is. A priority system (weapons always in slots 1–4, ammo in 5–8) would improve UX.
  • No drag-and-drop in UI yet. The inventory logic supports rearranging slots; the UI doesn't expose it.

Using This System in Your Project

  1. Copy ItemData.cs, InventorySlot.cs, InventoryManager.cs, ItemRegistry.cs into your project
  2. Create an ItemRegistry asset in Assets/Data/
  3. Create ItemData assets for each item type
  4. Instantiate InventoryManager on your player controller
  5. Subscribe your inventory UI to OnInventoryChanged