Using Scriptable Objects as items - how to spawn items in the game?

As the title describes, I’m attempting to create an inventory system using Scriptable Objects as items. I’m able to create different instances of items in the projects folder, but how can I make these items show up inside the game world? Is it possible to instantiate an item during runtime (such as if a player drops an item on the floor from the inventory)?

For example, I have created three scriptable object assets under the SO class item. They are an apple, a sword and wood. Let’s say I wanted to instantiate a wood item when the player drops it from his inventory. How would I go about referencing the scriptable object’s data?

Any help would be appreciated. Thanks!

You could have the world game object referenced in the scriptable object so you know what to spawn. On that item, you could have a script that has a variable for the scriptable object.

So it just happens this past day I’ve spent doing a lot of working on my inventory system (upgrading the UI for it).

I’ll show my inventory item:

using UnityEngine;
using System.Collections.Generic;

using com.spacepuppy;
using com.mansion.Entities.Actors.Player;

namespace com.mansion.Entities.Inventory
{

    [CreateAssetMenu(fileName = "InventoryItem", menuName = "Prototype Mansion/InventoryItem")]
    public class InventoryItem : ScriptableObject
    {

        #region Fields

        [SerializeField]
        private Sprite _icon;

        [SerializeField]
        private InventoryItemViewUI _menuVisual;
       
        [SerializeField]
        private string _title;

        [SerializeField]
        private string _description;

        [SerializeField]
        [Tooltip("Items that are unique can only be added to the InventoryPouch once. Re-adding it doesn't do anything.")]
        private bool _unique = true;

        #endregion

        #region CONSTRUCTOR
       
        #endregion

        #region Properties
       
        public Sprite Icon
        {
            get { return _icon; }
            set { _icon = value; }
        }

        public InventoryItemViewUI MenuVisual
        {
            get { return _menuVisual; }
            set { _menuVisual = value; }
        }

        public string Title
        {
            get { return _title; }
            set { _title = value; }
        }

        public string Description
        {
            get { return _description; }
            set { _description = value; }
        }

        public bool Unique
        {
            get { return _unique; }
            set { _unique = value; }
        }

        #endregion

        #region Methods

        public virtual void OnItemAddedToInventory(InventoryPouch pouch)
        {
            //do nothing
        }

        public virtual void OnItemRemovedFromInventory(InventoryPouch pouch)
        {
            //do nothing
        }
       
        public virtual bool PlayerHasInInventory(PlayerEntity player, bool isEquipped)
        {
            if (isEquipped) return false;

            var inventory = player.GetComponent<InventoryPouch>();
            if (inventory == null) return false;

            return inventory.Items.Contains(this);
        }
       
        #endregion

    }

}

This is the base implementation of my InventoryItem ScriptableObject.

Note it has fields for title, description, the icon (though this is being obsoleted), and the ‘menuView’. The menuView is a reference to a prefab that is used in menus for the item:
3494907--278426--Inventory.gif

Here is a more specific class that inherits from InventoryItem, WeaponInventoryItem, which has in world visuals as well:

using UnityEngine;
using System.Collections.Generic;
using System.Linq;

using com.spacepuppy;

using com.mansion.Entities.Actors.Player;
using com.mansion.Entities.Inventory;

namespace com.mansion.Entities.Weapons
{

    [CreateAssetMenu(fileName = "WeaponInventoryItem", menuName = "Prototype Mansion/WeaponInventoryItem")]
    public class WeaponInventoryItem : InventoryItem
    {

        #region Fields

        [SerializeField]
        [TypeRestriction(typeof(IWeapon))]
        private Component _prefab;

        [SerializeField]
        [ReorderableArray]
        [UnityEngine.Serialization.FormerlySerializedAs("_auxInfo")]
        private List<PlayerAuxInfo> _auxInfos;

        #endregion

        #region Properties

        public IWeapon Prefab
        {
            get { return _prefab as IWeapon; }
            set { _prefab = value as Component; }
        }

        public List<PlayerAuxInfo> AuxInfos
        {
            get { return _auxInfos; }
        }

        #endregion

        #region Methods

        public override void OnItemAddedToInventory(InventoryPouch pouch)
        {
            base.OnItemAddedToInventory(pouch);

            if (!object.ReferenceEquals(_prefab, null))
            {
                var weaponPouch = pouch.Entity.GetComponent<WeaponPouch>();
                if (weaponPouch != null)
                {
                    var weapon = Instantiate(_prefab, weaponPouch.WeaponBone) as IWeapon;
                    var aux = this.GetPlayerWeaponAux((pouch.Entity is PlayerEntity) ? (pouch.Entity as PlayerEntity).PlayerId : PlayerId.Unknown);
                    if (aux != null)
                    {
                        aux = Instantiate(aux, weapon.transform);
                        aux.transform.localPosition = Vector3.zero;
                        aux.transform.localRotation = Quaternion.identity;
                    }

                    var hook = weapon.gameObject.AddComponent<ItemHook>();
                    hook.Item = this;

                    weaponPouch.Weapons.Add(weapon);
                }
            }
        }

        public override void OnItemRemovedFromInventory(InventoryPouch pouch)
        {
            base.OnItemRemovedFromInventory(pouch);

            var weaponPouch = pouch.Entity.GetComponent<WeaponPouch>();
            if (weaponPouch != null)
            {
                var weapon = this.GetRelatedWeaponOnPlayer(weaponPouch);
                if (weapon != null)
                {
                    weaponPouch.Weapons.Remove(weapon);
                    Destroy(weapon.gameObject);
                }
            }
        }

        public PlayerWeaponAux GetPlayerWeaponAux(PlayerId id)
        {
            for (int i = 0; i < _auxInfos.Count; i++)
            {
                if ((_auxInfos[i].Player & id) != 0) return _auxInfos[i].WeaponAux;
            }

            return null;
        }

        public override bool PlayerHasInInventory(PlayerEntity player, bool isEquipped)
        {
            if (!base.PlayerHasInInventory(player, false)) return false;
            if (!isEquipped) return true;

            var pouch = player.GetComponent<PlayerWeaponPouch>();
            if (pouch == null) return false;

            var weapon = this.GetRelatedWeaponOnPlayer(pouch);
            if (weapon == null) return false;

            return pouch.CurrentWeapon == weapon;
        }

        public IWeapon GetRelatedWeaponOnPlayer(WeaponPouch pouch)
        {
            //return (from w in pouch.Weapons
            //        let h = w.gameObject.GetComponent<ItemHook>()
            //        where !object.ReferenceEquals(h, null) &&
            //              h.Item == this
            //        select w).FirstOrDefault();
            var e = pouch.Weapons.GetEnumerator();
            while(e.MoveNext())
            {
                var h = e.Current.gameObject.GetComponent<ItemHook>();
                if (!object.ReferenceEquals(h, null) && h.Item == this) return e.Current;
            }
            return null;
        }


        #endregion

        #region Special Types

        [System.Serializable]
        public struct PlayerAuxInfo
        {
            [EnumFlags]
            public PlayerId Player;
            public PlayerWeaponAux WeaponAux;
        }

        public class ItemHook : MonoBehaviour
        {

            public WeaponInventoryItem Item
            {
                get;
                internal set;
            }

        }

        #endregion

        #region Static Utils

        public static WeaponInventoryItem GetFromWeapon(IWeapon weapon)
        {
            if (weapon == null) return null;
            var hook = weapon.gameObject.GetComponent<ItemHook>();
            return hook != null ? hook.Item : null;
        }

        #endregion

    }

}

Note this one has a ‘prefab’ field which stores the in game prefab which is added to the player when added to inventory.

Note how I have an ‘ItemHook’ that links back to the WeaponInventoryItem from the in world weapon visuals. This way I can look up the item from the weapon.

You can ignore the ‘PlayerAuxInfo’. That’s unique to my system. There are 3 distinct players in my game, and they have different movement settings, animations, etc, based on which weapon they have. The auxiliary info houses the unique data on a per player, per weapon, basis.

The prefab is just the visual container.

I only ever instantiate the item in the player’s hand (hence the OnItemAddedToInventory implementation). But you could also spawn the prefab in the real world to be ‘droppable’.

1 Like

Sorry, could you clarify what you mean by having the world game object referenced in the scriptable object? How do I go about doing this? If I instantiate an empty prefab, how does it get the “data” from the asset file?

Wow, your code is way over my head haha. It seems that your scriptable object is trying to reference the inventory script? How does this work? If for example I have a unique instance of the scriptable object “Item” which holds different name, description etc. how do I call it from the inventory script to spawn as an in game object?

It’s not trying to reference the inventory script.

When the inventory script has an item added to it. It is handed a reference to the InventoryItem scriptable object. It then calls the ‘OnItemAddedToInventory’ method on the inventory item, passing itself along, to signal to it that it’s now a member of an inventory pouch.

And when you go and remove it, I signal to it that it’s been removed. So it can clean itself up.

Hmm the problem I’m having is with the reference, how did you pass the scriptable object reference to the Inventory script? Also, I’m trying to have unique instance of the scriptable object item to have stuff like durability be unique for each instance of the item.

Making them unique is no big deal, you’d just instantiate an instance of your ScriptableObject… done.

As for how I personally pass my reference (note I’m just showing my code as an example) is from the InventoryPouch which acts as the container for items as they get added. It’s just a signal of an event occurring… it could really be called by anything.

I happen to call it during the ‘Add’ code of my InventoryPouch here:

using UnityEngine;
using System.Collections.Generic;
using System.Linq;

using com.spacepuppy;
using com.spacepuppy.Dynamic;
using com.spacepuppy.Project;
using com.spacepuppy.Scenario;
using com.spacepuppy.Utils;

namespace com.mansion.Entities.Inventory
{

    public class InventoryPouch : SPComponent, ITokenizable
    {
     
        #region Fields

        [SerializeField]
        [ReorderableArray]
        [DisableOnPlay]
        private List<InventoryItem> _items = new List<InventoryItem>();
        [System.NonSerialized]
        private ItemCollection _itemColl;

        [SerializeField]
        private Trigger _onInventoryChanged;

        [System.NonSerialized]
        private IEntity _entity;
     
        #endregion

        #region CONSTRUCTOR

        protected override void Awake()
        {
            base.Awake();

            _entity = IEntity.Pool.GetFromSource<IEntity>(this);
        }

        protected override void Start()
        {
            base.Start();

            if (!this.IsInitialized) this.InitItems();
        }

        protected virtual void InitItems()
        {
            this.IsInitialized = true;

            for (int i = 0; i < _items.Count; i++)
            {
                var item = _items[i];
                if (item == null)
                {
                    _items.RemoveAt(i);
                    i--;
                }
                else
                {
                    this.OnItemAdded(item);
                }
            }
        }

        #endregion

        #region Properties

        public IEntity Entity
        {
            get { return _entity; }
        }

        public bool IsInitialized
        {
            get;
            private set;
        }

        public ItemCollection Items
        {
            get
            {
                if (_itemColl == null) _itemColl = new ItemCollection(this);
                return _itemColl;
            }
        }

        public Trigger OnInventoryChanged
        {
            get { return _onInventoryChanged; }
        }
     
        #endregion

        #region Methods

        protected virtual void OnItemAdded(InventoryItem item)
        {
            if (!this.IsInitialized) this.InitItems();

            item.OnItemAddedToInventory(this);

            if (_onInventoryChanged.Count > 0) _onInventoryChanged.ActivateTrigger(this, this);
        }

        protected virtual void OnItemRemoved(InventoryItem item)
        {
            item.OnItemRemovedFromInventory(this);

            if (_onInventoryChanged.Count > 0) _onInventoryChanged.ActivateTrigger(this, this);
        }

        protected virtual void OnClearingItems()
        {
            foreach(var item in _items)
            {
                if (!object.ReferenceEquals(item, null)) item.OnItemRemovedFromInventory(this);
            }

            if (_onInventoryChanged.Count > 0) _onInventoryChanged.ActivateTrigger(this, this);
        }

        #endregion

        #region ITokenizable Interface

        public object CreateStateToken()
        {
            return (from i in _items select i.name).ToArray();
        }

        public void RestoreFromStateToken(object token)
        {
            this.Items.Clear();

            var arr = token as string[];

            if (arr != null && arr.Length > 0)
            {
                var items = Game.Settings.InventorySet;

                if (!object.ReferenceEquals(items, null))
                {
                    foreach (var sinv in arr)
                    {
                        var item = items.GetAsset(sinv) as InventoryItem;
                        if (!object.ReferenceEquals(item, null))
                        {
                            this.Items.Add(item);
                        }
                    }

                    Resources.UnloadAsset(items);
                    Resources.UnloadUnusedAssets();
                }
            }
        }
     
        #endregion

        #region Special Types

        public class ItemCollection : IList<InventoryItem>
        {

            private InventoryPouch _owner;

            public ItemCollection(InventoryPouch pouch)
            {
                if (pouch == null) throw new System.ArgumentNullException("pouch");
                _owner = pouch;
            }


            public int Count { get { return (_owner._items != null) ? _owner._items.Count : 0; } }

            public bool IsReadOnly { get { return false; } }

            public InventoryItem this[int index]
            {
                get { return _owner._items[index]; }
            }

            public void Add(InventoryItem inv)
            {
                if (inv == null) throw new System.ArgumentNullException("inv");

                if (!inv.Unique || !_owner._items.Contains(inv))
                {
                    _owner._items.Add(inv);
                    _owner.OnItemAdded(inv);
                }
            }

            public bool Remove(InventoryItem inv)
            {
                if (inv == null) return false;

                if (_owner._items.Remove(inv))
                {
                    _owner.OnItemRemoved(inv);
                    return true;
                }
                else
                {
                    return false;
                }
            }

            public void Clear()
            {
                _owner.OnClearingItems();
                if (_owner._items != null) _owner._items.Clear();
            }

            public bool Contains(InventoryItem inv)
            {
                return _owner._items.Contains(inv);
            }

            public int CountItem(InventoryItem inv)
            {
                if (inv == null) return 0;

                int cnt = 0;
                var e = _owner._items.GetEnumerator();
                while (e.MoveNext())
                {
                    if (e.Current == inv) cnt++;
                }
                return cnt;
            }

            public void CopyTo(InventoryItem[] array, int arrayIndex)
            {
                for (int i = 0; i < _owner._items.Count; i++)
                {
                    array[arrayIndex + i] = _owner._items[i];
                }
            }

            public IEnumerator<InventoryItem> GetEnumerator()
            {
                for (int i = 0; i < _owner._items.Count; i++)
                {
                    yield return _owner._items[i];
                }
            }

            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return this.GetEnumerator();
            }

            public int IndexOf(InventoryItem item)
            {
                return _owner._items.IndexOf(item);
            }

            public void RemoveAt(int index)
            {
                if (index < 0 || index >= _owner._items.Count) return;

                var weapon = _owner._items[index];
                _owner._items.RemoveAt(index);
                _owner.OnItemRemoved(weapon);
            }

            void IList<InventoryItem>.Insert(int index, InventoryItem inv)
            {
                if (inv == null) throw new System.ArgumentNullException("inv");

                if (!inv.Unique || !_owner._items.Contains(inv))
                {
                    _owner._items.Insert(index, inv);
                    _owner.OnItemAdded(inv);
                }
            }

            InventoryItem IList<InventoryItem>.this[int index]
            {
                get { return _owner._items[index]; }
                set
                {
                    throw new System.NotSupportedException();
                }
            }

        }
     
#endregion

    }

}

Specifically in this section:

        #region Methods

        protected virtual void OnItemAdded(InventoryItem item)
        {
            if (!this.IsInitialized) this.InitItems();

            item.OnItemAddedToInventory(this);

            if (_onInventoryChanged.Count > 0) _onInventoryChanged.ActivateTrigger(this, this);
        }

        protected virtual void OnItemRemoved(InventoryItem item)
        {
            item.OnItemRemovedFromInventory(this);

            if (_onInventoryChanged.Count > 0) _onInventoryChanged.ActivateTrigger(this, this);
        }

        protected virtual void OnClearingItems()
        {
            foreach(var item in _items)
            {
                if (!object.ReferenceEquals(item, null)) item.OnItemRemovedFromInventory(this);
            }

            if (_onInventoryChanged.Count > 0) _onInventoryChanged.ActivateTrigger(this, this);
        }

        #endregion

The action goes as follows.

Player walks up on item.
Player presses button.
PlayerActionMotor scans local region for things that can be interacted with.
Item pickup is found.
Item pickup has ‘Interact’ called on it, passing along a reference to the player script that did the interacting.
Interact grabs reference to InventoryPouch on the player that interacted with it.
Interact passes a reference to the ‘InventoryItem’ scriptable object into the InventoryPouch.Items.Add function (here is when you might duplicate the item).
InventoryPouch adds the InventoryItem into its List.
InventoryPouch calls OnItemAddedToInventory on the InventoryItem specifically.
InventoryItem does whatever it must when it was added (in my case, if it was a weapon it creates the visual of it and sticks it in the player’s bone).
That function returns.
Interact deletes world item.

Hmm okay, but what if the item is NOT in the world (i.e. it only exists as an asset within the projects folder and you are trying to instantiate it? For example, when an enemy dies, it spawns loot. Can I dynamically “assign” item data onto empty prefabs after spawning them?

If I have AddItem(item) where item is a scriptable object, how can I parse in the item data that only exists within the projects folder? Sorry if I’m not being clear.

using UnityEngine;

[CreateAssetMenu(fileName = "Item", menuName = "Inventory/Item")]
public class Item : ScriptableObject {

    public string itemName = "New Item";
    public bool isStackable = false;
    public int maxStackSize = 20;
    public bool isUnique = false; // Only one copy of this item should exist in a game instance.
    public Sprite icon = null;
}

This is all I have for my item scriptable object for now. And I’ve created a few… “instances”(?) in my project folder with different variables. I want to pass this information to spawn a gameobject within the world. It seems like if want to spawn an instance of an already created asset, I have to do it like

MyScriptableObject myObj = Resources.Load('asset_name') as MyScriptableObject;

You’d just do the inverse.

The InventoryItem ScriptableObject would have a prefab on it. And you’d instantiate that into the world when necessary.

Such as when the item is removed from inventory. Or on initial creation. You just need to create the hooks into it that are necessary.

I could hook up my item ScriptableObject with a prefab, but how would I create a reference to that prefab from my inventory script? I cannot link that prefab to be instantiated because I will not have the reference to it?

1 Like