Build a Robust Unity Inventory System with ScriptableObjects
Let’s face it: most tutorials on Unity inventory systems are insultingly basic. They show you how to create a thing, but not why you should create it that way, or the trade-offs you’re making. We’re going to fix that. This isn’t just another “drag-and-drop” tutorial. This is about building a robust, scalable, and maintainable inventory system using ScriptableObjects and Unity’s UI system. We’ll skip the “shimmering reflections” and get straight to building a practical system.
The Opinionated Foundation: ScriptableObjects for Item Definitions
Why ScriptableObjects? Because hardcoding item data is a recipe for disaster. Imagine trying to change the damage value of a “Rusty Sword” in 50 different places throughout your codebase. ScriptableObjects provide a single source of truth for item definitions.
Create a new C# script named ItemData.cs
:
using UnityEngine;
[CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item")]
public class ItemData : ScriptableObject
{
public string itemName;
public string itemDescription;
public Sprite itemIcon;
public int maxStackSize = 1; // Assume single item stacks by default.
public bool isStackable = false; //Is item stackable?
// Add any other item-specific properties here, like damage, armor, etc.
}
Now, right-click in your Project window, go to Create -> Inventory -> Item. Name it “Rusty Sword.” Fill in the details. Create a few more items – a potion, some gold coins (make these stackable!), and maybe some armor.
Challenge: What happens if you want different types of items with different properties? A sword might have a damage value, while armor has a defense value. Inheritance to the rescue!
Create a WeaponData.cs
script:
using UnityEngine;
public class WeaponData : ItemData
{
public int damage;
}
And an ArmorData.cs
script:
using UnityEngine;
public class ArmorData : ItemData
{
public int defense;
}
Now, when you create a new weapon, it inherits all the basic item data plus a damage value. This keeps your data organized and extensible. Remember to change the CreateAssetMenu
attribute in each script if you want direct creation of those types.
The Inventory Manager: Logic is King
The Inventory Manager is the brain of your system. It handles adding, removing, and managing items.
Create a new C# script called InventoryManager.cs
:
using System.Collections.Generic;
using UnityEngine;
public class InventoryManager : MonoBehaviour
{
public static InventoryManager Instance; // Singleton pattern for easy access
public List<InventorySlot> inventory = new List<InventorySlot>();
public int inventorySize = 20; // Set default Inventory size.
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(this);
}
else
{
Instance = this;
DontDestroyOnLoad(gameObject); // Optional: Persist between scenes.
}
//Initialize Inventory slots, important for the UI!
for (int i = 0; i < inventorySize; i++)
{
inventory.Add(new InventorySlot());
}
}
public bool AddItem(ItemData item, int amount = 1)
{
// First, check if we can add the items to an existing stack.
if (item.isStackable)
{
for (int i = 0; i < inventory.Count; i++)
{
InventorySlot slot = inventory[i];
if (slot.item == item)
{
//If there is a matching item, increase the amount.
int amountAddable = item.maxStackSize - slot.amount;
if (amountAddable >= amount)
{
slot.amount += amount;
return true;
}
else
{
slot.amount = item.maxStackSize;
amount -= amountAddable;
}
}
}
}
//If there isn't an existing stack, add it to a new slot if there is space
if (amount <= 0) return true; // Everything was stacked successfully.
for (int i = 0; i < inventory.Count; i++)
{
InventorySlot slot = inventory[i];
if (slot.item == null)
{
slot.item = item;
slot.amount = amount;
return true;
}
}
Debug.Log("Inventory full!");
return false; // Inventory is full
}
public void RemoveItem(ItemData item, int amount = 1)
{
for (int i = 0; i < inventory.Count; i++)
{
InventorySlot slot = inventory[i];
if (slot.item == item)
{
if (slot.amount >= amount)
{
slot.amount -= amount;
if (slot.amount <= 0)
{
slot.item = null; // Remove the item from the slot.
}
return;
}
else
{
amount -= slot.amount;
slot.item = null;
}
}
}
}
}
[System.Serializable] // Make sure it shows up in the Inspector!
public class InventorySlot
{
public ItemData item;
public int amount;
}
Create an empty GameObject in your scene and attach this script to it.
Pitfall: Notice the Singleton pattern (InventoryManager.Instance
). This makes accessing the inventory from other scripts easy, but can lead to tightly coupled code. Overuse can make testing harder. Only use singletons when truly necessary!
Visualizing the Void: Unity UI Integration
Now for the fun part: showing the inventory on screen. This example uses a Grid Layout Group
for simplicity.
- Create a new Canvas in your scene.
- Inside the Canvas, create a Panel. This will be your inventory window.
- Inside the Panel, create a Grid Layout Group. This will arrange your inventory slots.
- Create a prefab for your inventory slot. This should contain an Image for the item icon and a Text component for the stack size.
Create a C# script called InventoryUI.cs
:
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
public class InventoryUI : MonoBehaviour
{
public Transform itemsParent; // Parent object for all item slots
public GameObject inventoryUIPrefab; // Prefab for each slot
InventoryManager inventoryManager;
List<InventorySlotUI> slots = new List<InventorySlotUI>(); //List of slots
void Start()
{
inventoryManager = InventoryManager.Instance;
//Create slots based on inventorySize
for (int i = 0; i < inventoryManager.inventorySize; i++)
{
GameObject newSlot = Instantiate(inventoryUIPrefab);
newSlot.transform.SetParent(itemsParent);
slots.Add(newSlot.GetComponent<InventorySlotUI>()); //Add to list
}
}
void Update()
{
UpdateUI();
}
void UpdateUI()
{
for (int i = 0; i < slots.Count; i++)
{
if (inventoryManager.inventory[i].item != null)
{
slots[i].AddItem(inventoryManager.inventory[i]);
}
else
{
slots[i].ClearSlot();
}
}
}
}
And a corresponding InventorySlotUI.cs
:
using UnityEngine;
using UnityEngine.UI;
public class InventorySlotUI : MonoBehaviour
{
public Image icon;
public Text amountText;
InventorySlot slot;
public void AddItem(InventorySlot newSlot)
{
slot = newSlot;
icon.sprite = slot.item.itemIcon;
icon.enabled = true;
if (slot.item.isStackable)
{
amountText.text = slot.amount.ToString();
}
else
{
amountText.text = ""; //Clear the text amount.
}
amountText.enabled = true;
}
public void ClearSlot()
{
slot = null;
icon.sprite = null;
icon.enabled = false;
amountText.text = "";
amountText.enabled = false;
}
}
Attach InventoryUI.cs
to your Panel. Drag your Grid Layout Group
to the Items Parent
field and your inventory slot prefab to the Inventory UIPrefab
field. Attach the InventorySlotUI.cs
to your inventory slot prefab and hookup the Icon and Amount Text fields.
Common Mistake: Forgetting to set the parent when instantiating the slot prefabs! This can lead to them being created outside the Canvas and not being visible. newSlot.transform.SetParent(itemsParent);
is your friend.
Now, in your game, you can call InventoryManager.Instance.AddItem()
to add items to the inventory. The InventoryUI
script will automatically update the UI to reflect the changes.
Beyond the Basics: Scalability and Polish
This is just the foundation. To build a truly great inventory system, consider these points:
- Item Pickup: Implement item spawning and pickup logic.
- Drag and Drop: Allow players to rearrange items in the inventory.
- Context Menus: Right-click on an item to use, equip, or drop it.
- Saving and Loading: Persist the inventory data between sessions.
- Performance: Optimize the UI updates, especially with large inventories. Object pooling could be useful.
By using ScriptableObjects for item definitions and a well-structured Inventory Manager, you’ve created a system that’s easy to extend and maintain. Don’t settle for basic tutorials. Understand the why behind the how, and you’ll be well on your way to building a truly impressive inventory system.