Inventory System · Part 1 of 2 in the series
Part 1: Items and the Inventory
Build the data side of an inventory in Unity: items as ScriptableObjects, stacks with an amount and one generic inventory class for every kind of item, tried out with pickups and a recipe.

Quest objectives
- Describe items as ScriptableObject assets
- Write one generic inventory for every kind of item
- Deposit and withdraw items without side effects
- Report changes to other code with C# events
- Pick up resources and craft with a recipe
Almost every game where players collect things needs an inventory: a place that stores items, counts them and hands them out again. This part builds one in Unity, for two kinds of items: collectibles, resources like wood or iron that the player gathers, and buildables, things like a storage building that the player makes from them and places in the world.
The two kinds have different data, but they should share all of the inventory's logic. Generics make that possible: one inventory class works for both, and for every kind you add later. The code is written for Unity 6.
The inventory item interface
An inventory stores stacks: an item together with how many there are of it. Every kind of stack has to offer the same four things, so they go into an interface. Create a C# script IInventoryItem.cs:
using UnityEngine;
// What every stack in an inventory has: the item, its type, its icon
// and how many there are.
public interface IInventoryItem<TItem, TItemType> {
TItem Item { get; set; }
TItemType Type { get; }
Sprite Icon { get; }
int Amount { get; set; }
}Item is the item itself, such as the wood asset. Type is what the inventory compares to find out whether two stacks hold the same thing. Icon is for the user interface in part 2, and Amount is how many there are. The two type parameters leave the kind of item and the kind of type open.
Collectibles
Collectibles are resources: wood, stone, iron, silver and gold. An enum names them. Create CollectibleItemType.cs:
public enum CollectibleItemType {
None = 0,
Wood = 1000,
Stone = 2000,
Iron = 3000,
Silver = 4000,
Gold = 5000,
}The gaps between the numbers leave room for related types later, such as Oak = 1001. Unity saves an enum field as its number, so never change the value of a type that is already in use.
The item itself is a ScriptableObject: an asset in the project that holds data, which every stack, pickup and recipe refers to. Create CollectibleItem.cs:
using UnityEngine;
[CreateAssetMenu(fileName = "New Collectible Item", menuName = "Items/Collectible Item")]
public class CollectibleItem : ScriptableObject {
public CollectibleItemType type;
public Sprite icon;
}Thanks to CreateAssetMenu you can now create items in the Project window: right-click, Create > Items > Collectible Item. Make five, Wood, Stone, Iron, Silver and Gold, and set their Type and Icon in the Inspector. Any sprite will do as an icon for now.
A stack of collectibles implements the interface. Create CollectibleInventoryItem.cs:
using System;
using UnityEngine;
[Serializable]
public class CollectibleInventoryItem : IInventoryItem<CollectibleItem, CollectibleItemType> {
[SerializeField] CollectibleItem item;
[SerializeField] int amount;
public CollectibleItem Item { get => item; set => item = value; }
public int Amount { get => amount; set => amount = value; }
public CollectibleItemType Type => item.type;
public Sprite Icon => item.icon;
}The class is [Serializable] and keeps its data in two [SerializeField] fields, because Unity saves fields, not properties. That makes stacks visible and editable in the Inspector, which you will use for testing and for recipes. Type and Icon come straight from the item asset.
Buildables
Buildables work the same way, with one addition: a prefab to place in the world. Again an enum first, BuildableItemType.cs:
public enum BuildableItemType {
None = 0,
Storage = 1000,
}The prefab gets a component of its own, so it can be told apart from other objects. It is empty for now. Create Buildable.cs:
using UnityEngine;
public class Buildable : MonoBehaviour {
}Then the item, BuildableItem.cs:
using UnityEngine;
[CreateAssetMenu(fileName = "New Buildable Item", menuName = "Items/Buildable Item")]
public class BuildableItem : ScriptableObject {
public BuildableItemType type;
public Sprite icon;
public Buildable prefab;
}Create one with Create > Items > Buildable Item, name it Storage and set its type to Storage. Last comes the stack of buildables, BuildableInventoryItem.cs, which also passes the prefab on:
using System;
using UnityEngine;
[Serializable]
public class BuildableInventoryItem : IInventoryItem<BuildableItem, BuildableItemType> {
[SerializeField] BuildableItem item;
[SerializeField] int amount;
public BuildableItem Item { get => item; set => item = value; }
public int Amount { get => amount; set => amount = value; }
public BuildableItemType Type => item.type;
public Sprite Icon => item.icon;
public Buildable Prefab => item.prefab;
}The inventory
Now the class that holds the stacks. It is generic over the stack, the item and the type, so the same code serves collectibles and buildables. Create Inventory.cs:
using System;
using System.Collections.Generic;
[Serializable]
public class Inventory<TInventoryItem, TItem, TItemType>
where TInventoryItem : class, IInventoryItem<TItem, TItemType>, new() {
public int capacity = 100;
public List<TInventoryItem> items = new List<TInventoryItem>();
// Raised with what went in or came out: the item and the amount that moved
public event Action<TInventoryItem> ItemDeposited;
public event Action<TInventoryItem> ItemWithdrawn;
public int Total {
get {
int total = 0;
foreach (TInventoryItem stack in items) {
total += stack.Amount;
}
return total;
}
}
public int FreeSpace => Math.Max(0, capacity - Total);
public bool IsFull => FreeSpace == 0;
public bool IsEmpty => Total == 0;
// Adds as much of the stack as fits and returns how much that was.
// The stack you pass in is copied, never stored or changed.
public int Deposit(TInventoryItem stack) {
int amount = Math.Min(stack.Amount, FreeSpace);
if (amount <= 0) {
return 0;
}
TInventoryItem existing = Find(stack.Type);
if (existing == null) {
items.Add(Create(stack.Item, amount));
} else {
existing.Amount += amount;
}
ItemDeposited?.Invoke(Create(stack.Item, amount));
return amount;
}
public int Deposit(List<TInventoryItem> stacks) {
int deposited = 0;
foreach (TInventoryItem stack in stacks) {
deposited += Deposit(stack);
}
return deposited;
}
// Takes up to amount of one type out.
// Returns what it got, or null if there was none.
public TInventoryItem Withdraw(TItemType type, int amount = 1) {
TInventoryItem existing = Find(type);
if (existing == null || amount <= 0) {
return null;
}
int taken = Math.Min(amount, existing.Amount);
existing.Amount -= taken;
if (existing.Amount == 0) {
items.Remove(existing);
}
TInventoryItem withdrawn = Create(existing.Item, taken);
ItemWithdrawn?.Invoke(withdrawn);
return withdrawn;
}
// Takes several stacks out, such as the cost of a recipe.
// Check Contains() first if it has to be all or nothing.
public List<TInventoryItem> Withdraw(List<TInventoryItem> stacks) {
List<TInventoryItem> withdrawn = new List<TInventoryItem>();
foreach (TInventoryItem stack in stacks) {
TInventoryItem taken = Withdraw(stack.Type, stack.Amount);
if (taken != null) {
withdrawn.Add(taken);
}
}
return withdrawn;
}
// Takes amount items out, whatever they are, oldest stack first.
public List<TInventoryItem> WithdrawAny(int amount) {
List<TInventoryItem> withdrawn = new List<TInventoryItem>();
while (amount > 0 && items.Count > 0) {
TInventoryItem taken = Withdraw(items[0].Type, amount);
if (taken.Amount > 0) {
amount -= taken.Amount;
withdrawn.Add(taken);
}
}
return withdrawn;
}
public bool Contains(List<TInventoryItem> stacks) {
foreach (TInventoryItem stack in stacks) {
if (GetAmount(stack.Type) < stack.Amount) {
return false;
}
}
return true;
}
public int GetAmount(TItemType type) {
TInventoryItem existing = Find(type);
return existing == null ? 0 : existing.Amount;
}
// The stack of this type, or null. There is never more than one per type.
public TInventoryItem Find(TItemType type) {
// Compares enums without boxing them, unlike Equals()
EqualityComparer<TItemType> comparer = EqualityComparer<TItemType>.Default;
foreach (TInventoryItem stack in items) {
if (comparer.Equals(stack.Type, type)) {
return stack;
}
}
return null;
}
static TInventoryItem Create(TItem item, int amount) {
TInventoryItem stack = new TInventoryItem();
stack.Item = item;
stack.Amount = amount;
return stack;
}
}What it does, from the top:
The constraints say what a stack has to be:
class, so a stack changes in place when the inventory adds to it; anIInventoryItem, so the inventory can read its type and amount; andnew(), so the inventory can create stacks of its own.capacitylimits the total amount, across all types.FreeSpace,IsFullandIsEmptyfollow from it and the stacks.Depositadds as much as fits and returns how much that was, so the caller knows what is left. There is only ever one stack per type: if there is one already, the amount goes onto it. The inventory never stores or changes the stack you pass in, it makes its own. Otherwise the caller could change amounts inside the inventory from outside, or pass the same stack in twice and double it.Withdrawtakes up to the given amount of one type out and returns it as a new stack, ornullif there is none. An empty stack is removed. The overload with a list takes several types at once, andWithdrawAnytakes a number of items of any type, oldest stack first, for example to unload everything into a storage building.Containschecks whether there is enough of everything on a list, which is what a recipe needs.Findcompares types withEqualityComparerrather thanEquals(), which boxes an enum in generic code and allocates memory on every comparison.ItemDepositedandItemWithdrawnreport every change, with what went in or came out. They areevents, so other classes can subscribe to them, but only the inventory can raise them. The user interface in part 2 listens to them.
An inventory for each kind of item
Written out, the type of an inventory for collectibles is Inventory<CollectibleInventoryItem, CollectibleItem, CollectibleItemType>. Two tiny classes give the two inventories short names. Create CollectibleInventory.cs:
using System;
[Serializable]
public class CollectibleInventory
: Inventory<CollectibleInventoryItem, CollectibleItem, CollectibleItemType> { }and BuildableInventory.cs:
using System;
[Serializable]
public class BuildableInventory
: Inventory<BuildableInventoryItem, BuildableItem, BuildableItemType> { }The player
The player carries one inventory of each kind. Create PlayerController.cs, or add the two fields to the player script you already have:
using UnityEngine;
public class PlayerController : MonoBehaviour {
public CollectibleInventory collectibles = new CollectibleInventory();
public BuildableInventory buildables = new BuildableInventory();
}Both inventories are serializable, so they show up in the Inspector with their capacity and stacks. Set the capacities there, for example 100 for collectibles and 5 for buildables. To start with some resources, fill the lists by hand.
If the player has to be reachable from everywhere, you can make it a singleton as in the quick tip The Singleton Pattern in Unity. This series gets by without one: code that needs the player gets it passed in or as a reference in the Inspector.
Picking things up
A pickup is a resource lying in the world, which goes into the player's inventory when the player walks into it. Create CollectiblePickup.cs:
using UnityEngine;
// A resource lying in the world. Walk into it to pick it up.
public class CollectiblePickup : MonoBehaviour {
public CollectibleItem item;
public int amount = 1;
void OnTriggerEnter(Collider other) {
PlayerController player = other.GetComponentInParent<PlayerController>();
if (player == null) {
return;
}
CollectibleInventoryItem stack = new CollectibleInventoryItem {
Item = item,
Amount = amount,
};
amount -= player.collectibles.Deposit(stack);
// Whatever didn't fit stays on the ground
if (amount == 0) {
Destroy(gameObject);
}
}
}GetComponentInParent also finds the player if its collider sits on a child object. Because Deposit returns how much fit, a full inventory leaves the rest on the ground.
Crafting with a recipe
A recipe turns collectibles into a buildable. It is an asset as well, with the cost as a list of collectible stacks. Create Recipe.cs:
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "New Recipe", menuName = "Items/Recipe")]
public class Recipe : ScriptableObject {
public List<CollectibleInventoryItem> cost = new List<CollectibleInventoryItem>();
public BuildableItem result;
// Pays the cost and adds the result, or does nothing at all
// if the player can't afford it or has no room for it
public bool TryCraft(PlayerController player) {
if (player.buildables.IsFull || !player.collectibles.Contains(cost)) {
return false;
}
player.collectibles.Withdraw(cost);
player.buildables.Deposit(new BuildableInventoryItem { Item = result, Amount = 1 });
return true;
}
}TryCraft checks first and changes second: only if there is room for the result and every resource is there in full does it take the cost out and put the result in. A failed attempt never costs anything.
Create a recipe with Create > Items > Recipe, name it StorageRecipe, add two entries to Cost, 10 Wood and 5 Stone, and drag Storage into Result.
To try it without writing input code, give the player a menu entry that crafts. Add this to PlayerController:
[SerializeField] Recipe storageRecipe;
// In the component's menu, the three dots in its header
[ContextMenu("Craft Storage")]
void CraftStorage() {
bool crafted = storageRecipe.TryCraft(this);
Debug.Log(crafted ? "Crafted a storage" : "Not enough resources, or no room");
}Trying it out
Create a capsule for the player. Add
PlayerControllerand aRigidbodywith Is Kinematic checked, so it doesn't fall, and drag StorageRecipe into Storage Recipe.Create a cube, check Is Trigger on its Box Collider, add
CollectiblePickup, drag Wood into Item and set Amount to 10. Duplicate it, drag Stone in and set the amount to 5.Press Play and drag the capsule into the cubes in the Scene view, or walk there with your own controller. The cubes disappear, and their resources show up under Collectibles in the player's Inspector.
Open the menu of the
PlayerControllercomponent, the three dots in its header, and choose Craft Storage. The wood and stone are gone, and a storage shows up under Buildables.
The project now has this structure:
Assets/
├── Objects/
│ ├── Collectibles/
│ │ ├── Wood.asset
│ │ ├── Stone.asset
│ │ ├── Iron.asset
│ │ ├── Silver.asset
│ │ └── Gold.asset
│ ├── Buildables/
│ │ └── Storage.asset
│ └── Recipes/
│ └── StorageRecipe.asset
└── Scripts/
├── Inventory/
│ ├── IInventoryItem.cs
│ └── Inventory.cs
├── Items/
│ ├── Collectibles/
│ │ ├── CollectibleItemType.cs
│ │ ├── CollectibleItem.cs
│ │ ├── CollectibleInventoryItem.cs
│ │ ├── CollectibleInventory.cs
│ │ └── CollectiblePickup.cs
│ ├── Buildables/
│ │ ├── BuildableItemType.cs
│ │ ├── Buildable.cs
│ │ ├── BuildableItem.cs
│ │ ├── BuildableInventoryItem.cs
│ │ └── BuildableInventory.cs
│ └── Recipe.cs
└── PlayerController.csThe inventory works, but the player can only see it in the Inspector. In the next part it gets a bar at the bottom of the screen that keeps up with every change.