October 4, 2025
Setting up an Inventory System with Scriptable Objects in Unity
Letting the player gather inventory throughout a game is commonplace these days, so let’s take a look at one way to set up an inventory…

By Jared Amlin
7 min read
Letting the player gather inventory throughout a game is commonplace these days, so let's take a look at one way to set up an inventory system. I know this system could be more generic, but I think it communicates an idea on how to approach inventory in Unity with the help of Scriptable Objects. Let's go!
I'm just prototyping here, so this Health Potion is represented by a blue cube primitive. The Collectable script has a reference to an Item, which is a Scriptable Object.
Here we have an Ammo pickup using the same Collectable script, only with a different Scriptable Object assigned to the Item slot.
Finally, this cube represents a Keycard pickup, which can be used to open a specific door.
The ItemSO class inherits from Scriptable Object, and the Create Asset Menu let's a designer use the Create menu option to make new scriptable objects. This class holds all of the base values shared between each item.
using UnityEngine;
[CreateAssetMenu(fileName = "item.asset", menuName = "Inventory/Item")]
public class ItemSO : ScriptableObject
{
public int itemID;
public string itemName;
public string itemDescription;
public int value;
public Sprite itemIcon;
public GameObject itemObj;
public void ItemName()
{
Debug.Log("This item is a " + itemName);
}
}using UnityEngine;
[CreateAssetMenu(fileName = "item.asset", menuName = "Inventory/Item")]
public class ItemSO : ScriptableObject
{
public int itemID;
public string itemName;
public string itemDescription;
public int value;
public Sprite itemIcon;
public GameObject itemObj;
public void ItemName()
{
Debug.Log("This item is a " + itemName);
}
}The Inventory Item class inherits from ItemSO, also making it a scriptable object. In addition to the other base stats, it has an Enum to store an item type, an additional restore value, and an Item Behavior method. The Item Behavior method uses a simple switch statement to break up the logic depending on what type of item it is.
One way to approach this would be to have a static Inventory Manager Singleton class. That may be a better option after all, but I want to explore using events here. I have a few different Action events that are used depending on the item type being collected.
The onHealthCollected event simply fires off if there are any listeners. The onAmmoCollected event passes in the restore value to be used as an ammo refill amount. The onKeyCardCollected event passes in the Inventory Item instance being used by this script.
using UnityEngine;
using System;
[CreateAssetMenu(fileName = "item.asset", menuName = "Inventory/Item/Consumable")]
public class InventoryItem : ItemSO
{
public static event Action onHealthCollected;
public static event Action<int> onAmmoCollected;
public static event Action<InventoryItem> onKeyCardCollected;
public enum ItemType
{
Keycard,
Health,
Ammo,
Coins,
}
[SerializeField] private ItemType _itemType;
[SerializeField] private int _restoreValue;
public void ItemBehavior()
{
switch(_itemType)
{
case ItemType.Health:
Debug.Log("Health Pickup Collected");
onHealthCollected?.Invoke();
break;
case ItemType.Keycard:
Debug.Log("Keycard Collected");
InventoryItem item = this;
onKeyCardCollected?.Invoke(item);
break;
case ItemType.Coins:
break;
case ItemType.Ammo:
Debug.Log("Ammo Pickup Collected");
onAmmoCollected?.Invoke(_restoreValue);
break;
default:
break;
}
}
}using UnityEngine;
using System;
[CreateAssetMenu(fileName = "item.asset", menuName = "Inventory/Item/Consumable")]
public class InventoryItem : ItemSO
{
public static event Action onHealthCollected;
public static event Action<int> onAmmoCollected;
public static event Action<InventoryItem> onKeyCardCollected;
public enum ItemType
{
Keycard,
Health,
Ammo,
Coins,
}
[SerializeField] private ItemType _itemType;
[SerializeField] private int _restoreValue;
public void ItemBehavior()
{
switch(_itemType)
{
case ItemType.Health:
Debug.Log("Health Pickup Collected");
onHealthCollected?.Invoke();
break;
case ItemType.Keycard:
Debug.Log("Keycard Collected");
InventoryItem item = this;
onKeyCardCollected?.Invoke(item);
break;
case ItemType.Coins:
break;
case ItemType.Ammo:
Debug.Log("Ammo Pickup Collected");
onAmmoCollected?.Invoke(_restoreValue);
break;
default:
break;
}
}
}I can now use the Create menu to make a new Consumable item.
The Health Potion prefab using the Collectable script has the Health Pickup scriptable object assigned to it's Item assignment slot. I'll share the Collectable class here shortly, as it's been expanded upon since previous articles.
Selecting the Scriptable Object in the inspector enables a designer to fill out and assign all of the required values. Here is an example for the health potion.
Here is the Ammo data.
The first Key Card here will be used to unlock a specific door.
This second Key Card will be used to test if the door remains locked should I have the wrong key card in the Inventory Manager.
When the pickup is interacted with, the Collectable class will call the Item Behavior method on the assigned Inventory Item.
using UnityEngine;
public class Collectable : MonoBehaviour, iInteractable
{
[SerializeField] private GameObject _particleFX;
[SerializeField] private InventoryItem _item;
public void Interact()
{
Debug.Log("Collectable Interaction");
_item.ItemBehavior();
if (_particleFX != null)
Instantiate(_particleFX, transform.position, Quaternion.identity);
Destroy(this.gameObject);
}
}using UnityEngine;
public class Collectable : MonoBehaviour, iInteractable
{
[SerializeField] private GameObject _particleFX;
[SerializeField] private InventoryItem _item;
public void Interact()
{
Debug.Log("Collectable Interaction");
_item.ItemBehavior();
if (_particleFX != null)
Instantiate(_particleFX, transform.position, Quaternion.identity);
Destroy(this.gameObject);
}
}This Player Interactions script is attached to the Player armature, and is just used as an example here for pickups that are used when collected, and do not get sent to inventory.
When the Health Potion is collected, the Inventory Item will invoke the onHealthCollected event. The Player Interactions here listens in and fills the player health. The Ammo pickup operates similarly, while also passing in an int value which is used to add to the player ammo count.
using UnityEngine;
public class PlayerInteractions : MonoBehaviour
{
[SerializeField] private int _maxHealth, _currentHealth;
[SerializeField] private int _ammo = 0;
private void Start()
{
InventoryItem.onHealthCollected += InventoryItem_onHealthCollected;
InventoryItem.onAmmoCollected += InventoryItem_onAmmoCollected;
}
private void InventoryItem_onAmmoCollected(int value)
{
_ammo += value;
}
private void OnDisable()
{
InventoryItem.onHealthCollected -= InventoryItem_onHealthCollected;
InventoryItem.onAmmoCollected -= InventoryItem_onAmmoCollected;
}
private void InventoryItem_onHealthCollected()
{
_currentHealth = _maxHealth;
}
}using UnityEngine;
public class PlayerInteractions : MonoBehaviour
{
[SerializeField] private int _maxHealth, _currentHealth;
[SerializeField] private int _ammo = 0;
private void Start()
{
InventoryItem.onHealthCollected += InventoryItem_onHealthCollected;
InventoryItem.onAmmoCollected += InventoryItem_onAmmoCollected;
}
private void InventoryItem_onAmmoCollected(int value)
{
_ammo += value;
}
private void OnDisable()
{
InventoryItem.onHealthCollected -= InventoryItem_onHealthCollected;
InventoryItem.onAmmoCollected -= InventoryItem_onAmmoCollected;
}
private void InventoryItem_onHealthCollected()
{
_currentHealth = _maxHealth;
}
}The Door script has a reference to a Key Card item, which is the needed key card to unlock the door.
The main thing to see here is a Functional event, which passes on an Item scriptable object, and returns a bool value. When the door is interacted with, the Check Key Card method gets called, which fires off the event to check if the key card is correct before unlocking the door.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class Door : MonoBehaviour, iInteractable
{
[SerializeField] private bool _isOpen = false;
private bool _isLocked = true;
private Animator _animatior;
[SerializeField] private GameObject _lockMessageUI;
private WaitForSeconds _waitForSeconds;
private bool _isAnimating = false;
[SerializeField] GameObject _accessLight;
[SerializeField] private MeshRenderer _mr;
[SerializeField] private Material _emissiveMat;
private const string _emissiveColor = "_EmissiveColor";
private Color _lockedColor = Color.red;
private Color _unlockedColor = Color.green;
[SerializeField] private ItemSO _keyCard;
public static event Func<ItemSO, bool> onKeyCardCheck;
private void Start()
{
_animatior = GetComponent<Animator>();
_waitForSeconds = new WaitForSeconds(5.1f);
if (_animatior == null)
Debug.LogWarning("The Animator on the Door " + gameObject.name + " is NULL");
_mr = _accessLight.GetComponent<MeshRenderer>();
//get material
if (_mr != null)
{
_emissiveMat = _mr.material;
}
else
Debug.LogWarning("The Mesh Renderer on the Door Access object is NULL");
}
public void Interact()
{
//check for keycard
CheckKeyCard();
if (_isLocked)
{
//show lock message to disable after a time and fade out
if (_lockMessageUI != null)
{
_lockMessageUI.SetActive(true);
if (!_isAnimating)
{
_isAnimating = true;
StartCoroutine(DeactivateUIRoutine());
}
}
}
else
{
_isOpen = !_isOpen;
_animatior.SetBool("IsOpen", _isOpen);
}
}
private IEnumerator DeactivateUIRoutine()
{
yield return _waitForSeconds;
if (_lockMessageUI != null)
{
_lockMessageUI.SetActive(false);
}
_isAnimating = false;
}
private void UnlockDoor()
{
_isLocked = false;
_emissiveMat.color = _unlockedColor;
_emissiveMat.SetColor(_emissiveColor, _unlockedColor);
}
private void LockDoor()
{
_isLocked = true;
_emissiveMat.color = _lockedColor;
_emissiveMat.SetColor(_emissiveColor, _lockedColor);
}
private void CheckKeyCard()
{
//have inventory manager check for the keycard.
_isLocked = (bool)(onKeyCardCheck?.Invoke(_keyCard));
Debug.Log("checking keycard from door " + _isLocked);
if (_isLocked)
{
LockDoor();
}
else
UnlockDoor();
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class Door : MonoBehaviour, iInteractable
{
[SerializeField] private bool _isOpen = false;
private bool _isLocked = true;
private Animator _animatior;
[SerializeField] private GameObject _lockMessageUI;
private WaitForSeconds _waitForSeconds;
private bool _isAnimating = false;
[SerializeField] GameObject _accessLight;
[SerializeField] private MeshRenderer _mr;
[SerializeField] private Material _emissiveMat;
private const string _emissiveColor = "_EmissiveColor";
private Color _lockedColor = Color.red;
private Color _unlockedColor = Color.green;
[SerializeField] private ItemSO _keyCard;
public static event Func<ItemSO, bool> onKeyCardCheck;
private void Start()
{
_animatior = GetComponent<Animator>();
_waitForSeconds = new WaitForSeconds(5.1f);
if (_animatior == null)
Debug.LogWarning("The Animator on the Door " + gameObject.name + " is NULL");
_mr = _accessLight.GetComponent<MeshRenderer>();
//get material
if (_mr != null)
{
_emissiveMat = _mr.material;
}
else
Debug.LogWarning("The Mesh Renderer on the Door Access object is NULL");
}
public void Interact()
{
//check for keycard
CheckKeyCard();
if (_isLocked)
{
//show lock message to disable after a time and fade out
if (_lockMessageUI != null)
{
_lockMessageUI.SetActive(true);
if (!_isAnimating)
{
_isAnimating = true;
StartCoroutine(DeactivateUIRoutine());
}
}
}
else
{
_isOpen = !_isOpen;
_animatior.SetBool("IsOpen", _isOpen);
}
}
private IEnumerator DeactivateUIRoutine()
{
yield return _waitForSeconds;
if (_lockMessageUI != null)
{
_lockMessageUI.SetActive(false);
}
_isAnimating = false;
}
private void UnlockDoor()
{
_isLocked = false;
_emissiveMat.color = _unlockedColor;
_emissiveMat.SetColor(_emissiveColor, _unlockedColor);
}
private void LockDoor()
{
_isLocked = true;
_emissiveMat.color = _lockedColor;
_emissiveMat.SetColor(_emissiveColor, _lockedColor);
}
private void CheckKeyCard()
{
//have inventory manager check for the keycard.
_isLocked = (bool)(onKeyCardCheck?.Invoke(_keyCard));
Debug.Log("checking keycard from door " + _isLocked);
if (_isLocked)
{
LockDoor();
}
else
UnlockDoor();
}
}The Inventory Manager has a List of Item scriptable objects, and I use a temp item for removal from the list after the item is used. There is admittedly some brute force going on here, so feel free to clean up this code!
In Start, this class subscribes to the onKeyCardCollected event from the Inventory Item, and the functional onKeyCardCheck event from the door.
The onKeyCardCollected event will add the passed in item to the List in the Inventory Manager.
Because the Item is passed in directly with the onKeyCardCheck event, I just loop through the inventory and check to see of any of the items are the same. If we do indeed have the correct key card, the isLocked bool will become false and returned to the Door script.
While I am removing items after use here, I could add some logic to be able to choose which items are discarded after use, and which ones are retained.
using System.Collections.Generic;
using UnityEngine;
public class InventoryManager : MonoBehaviour
{
public List<ItemSO> _inventoryItems = new List<ItemSO>();
private ItemSO _tempItem;
private void Start()
{
InventoryItem.onKeyCardCollected += InventoryItem_onKeyCardCollected;
Door.onKeyCardCheck += Door_onKeyCardCheck;
}
private bool Door_onKeyCardCheck(ItemSO keyCard)
{
Debug.Log("Door checking");
bool isLocked = true;
foreach(ItemSO item in _inventoryItems)
{
Debug.Log("Foreach loop running");
if (item == keyCard)
{
Debug.Log("Keycard name found!");
_tempItem = item;
//unlock door. remove keycard
//RemoveItem(item); //dont adjust a list in a foreach loop
isLocked = false;
}
}
if (_inventoryItems.Count > 0)
{
if (_tempItem != null)
RemoveItem(_tempItem);
}
return isLocked;
}
private void OnDisable()
{
InventoryItem.onKeyCardCollected -= InventoryItem_onKeyCardCollected;
Door.onKeyCardCheck -= Door_onKeyCardCheck;
}
private void InventoryItem_onKeyCardCollected(InventoryItem item)
{
AddItem(item);
}
private void AddItem(ItemSO item)
{
_inventoryItems.Add(item);
}
private void RemoveItem(ItemSO item)
{
_inventoryItems.Remove(item);
}
}using System.Collections.Generic;
using UnityEngine;
public class InventoryManager : MonoBehaviour
{
public List<ItemSO> _inventoryItems = new List<ItemSO>();
private ItemSO _tempItem;
private void Start()
{
InventoryItem.onKeyCardCollected += InventoryItem_onKeyCardCollected;
Door.onKeyCardCheck += Door_onKeyCardCheck;
}
private bool Door_onKeyCardCheck(ItemSO keyCard)
{
Debug.Log("Door checking");
bool isLocked = true;
foreach(ItemSO item in _inventoryItems)
{
Debug.Log("Foreach loop running");
if (item == keyCard)
{
Debug.Log("Keycard name found!");
_tempItem = item;
//unlock door. remove keycard
//RemoveItem(item); //dont adjust a list in a foreach loop
isLocked = false;
}
}
if (_inventoryItems.Count > 0)
{
if (_tempItem != null)
RemoveItem(_tempItem);
}
return isLocked;
}
private void OnDisable()
{
InventoryItem.onKeyCardCollected -= InventoryItem_onKeyCardCollected;
Door.onKeyCardCheck -= Door_onKeyCardCheck;
}
private void InventoryItem_onKeyCardCollected(InventoryItem item)
{
AddItem(item);
}
private void AddItem(ItemSO item)
{
_inventoryItems.Add(item);
}
private void RemoveItem(ItemSO item)
{
_inventoryItems.Remove(item);
}
}Here is the player collecting the Health Potion.
The player has now been fully healed!
Here is the Ammo pickup in action.
The player has 50 rounds added to their reserves.
Time to collect the key card and progress through this room.
The Inventory Manager now has a reference to the collected key card.
Now when the player interacts with the door, access is granted!
After being used, the key card has now been removed from the List of Inventory Items.
Here I assign another key card to test if this is working properly.
As expected, we can not enter without the proper key card.
I hope you enjoyed this voyage into inventory in Unity, and thanks for reading!