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:
- Items defined in data, not code — adding a new item should take 5 minutes, not an hour
- Stacking — identical items merge up to a configurable max stack
- UI-decoupled — inventory logic should work without any UI attached
- 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
TryAddis called withamount > 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
- Copy
ItemData.cs,InventorySlot.cs,InventoryManager.cs,ItemRegistry.csinto your project - Create an
ItemRegistryasset inAssets/Data/ - Create
ItemDataassets for each item type - Instantiate
InventoryManageron your player controller - Subscribe your inventory UI to
OnInventoryChanged
Related
- Sharpshooters — where this system was first built and tested
- State Machines in Unity — the player state machine that interacts with inventory