Saving Inventory Between Scenes (Scriptable Objects)

Announcements, support questions, and discussion for the Dialogue System.
Post Reply
DrewThomasArt
Posts: 60
Joined: Thu Mar 24, 2022 12:07 am

Saving Inventory Between Scenes (Scriptable Objects)

Post by DrewThomasArt »

Hello,
I've mastered saving serializable things like ints and strings using the method in this Pixel Crushers tutorial:
https://pixelcrushers.com/phpbb/viewtop ... f=9&t=4763

I've been stuck for a few days on saving my inventory though. I'm trying to use this method from the tutorial:

Code: Select all

public class InventorySaveData
{
    public int money;
    public List<string> itemNames;
}

public override string RecordData()
{
    var inventory = GetComponent<Inventory>(); // Example -- get your inventory system script.
    var saveData = new InventorySaveData();
    saveData.money = inventory.money;
    saveData.itemNames = new List<string>();
    foreach (var item in inventory.item)
    {
        saveData.itemNames.Add(item.name);
    }
    return SaveSystem.Serialize(saveData); // Return serialized data.
}
The problem is on this line:

Code: Select all

foreach (var item in inventory.item)
This is my version:

Code: Select all

public string RecordData()
{
    var inventory = GetComponent<InventorySO>(); // Example -- get your inventory system script.
    var saveData = new InventorySaveData();
    saveData.itemNames = new List<string>();
    foreach (var item in inventory.item)
    {
        saveData.itemNames.Add(item.name);
    }
    return SaveSystem.Serialize(saveData);
}
I get this error:
foreach statement cannot operate on variables of type 'ItemSO' because 'ItemSO' does not contain a public instance or extension definition for 'GetEnumerator'

I tried adding a public Enumerator method to my ItemSO but then it requires more to be implemented, and I have no idea what to implement, I can't find it online, and I don't even know if that's the solution anyways.
Before I switch to the Opsive Ultimate Inventory, I want to ask here because it took a couple of weeks to set this up.
Below are the InventorySO and ItemSO scripts. I feel like pulling the item names should be simple, but I can't get past this error. Thanks for any help!

My InventorySO script:

Code: Select all

namespace Inventory.Model
 {
    [CreateAssetMenu]

    public class InventorySO : ScriptableObject
    {
   public ItemSO item;

         [SerializeField]
         public List<InventoryItem> inventoryItems;

         [field: SerializeField]
         public int Size { get; private set; } = 30;



        public event Action<Dictionary<int, InventoryItem>> OnInventoryUpdated;

        public void Initialize()
        {
            inventoryItems = new List<InventoryItem>();
            for (int i = 0; i < Size; i++)
            {
                inventoryItems.Add(InventoryItem.GetEmptyItem());
            }
        }

  

        public int AddItem(ItemSO item, int quantity)
        {
   if(item.IsStackable == false)
            {
                for (int i = 0; i < inventoryItems.Count; i++)
                {
                    while(quantity > 0 && IsInventoryFull() == false)
                    {
                        quantity -= AddItemToFirstFreeSlot(item, 1);
                    }
                    InformAboutChange();
                    return quantity;
                }
            }
            quantity = AddStackableItem(item, quantity);
            InformAboutChange();
            return quantity;
        }

          private int AddItemToFirstFreeSlot(ItemSO item, int quantity
          , List<ItemParameter> itemState = null)
        {
            InventoryItem newItem = new InventoryItem
            {
                item = item,
                quantity = quantity,
                itemState = 
                new List<ItemParameter>(itemState == null ? item.DefaultParametersList : itemState)
            };

            for (int i = 0; i < inventoryItems.Count; i++)
            {
                if (inventoryItems[i].IsEmpty)
                {
                    inventoryItems[i] = newItem;
                    return quantity;
                }
            }
            return 0;
        }

        private bool IsInventoryFull()
            => inventoryItems.Where(item => item.IsEmpty).Any() == false;

   private int AddStackableItem(ItemSO item, int quantity)
        {
            for (int i = 0; i < inventoryItems.Count; i++)
            {
                if (inventoryItems[i].IsEmpty)
                    continue;
                if(inventoryItems[i].item.ID == item.ID)
                {
                    int amountPossibleToTake =
                        inventoryItems[i].item.MaxStackSize - inventoryItems[i].quantity;

                    if (quantity > amountPossibleToTake)
                    {
                        inventoryItems[i] = inventoryItems[i]
                            .ChangeQuantity(inventoryItems[i].item.MaxStackSize);
                        quantity -= amountPossibleToTake;
                    }
                    else
                    {
                        inventoryItems[i] = inventoryItems[i]
                            .ChangeQuantity(inventoryItems[i].quantity + quantity);
                        InformAboutChange();
                        return 0;
                    }
                }
            }
            while(quantity > 0 && IsInventoryFull() == false)
            {
                int newQuantity = Mathf.Clamp(quantity, 0, item.MaxStackSize);
                quantity -= newQuantity;
                AddItemToFirstFreeSlot(item, newQuantity);
            }
            return quantity;
        }

        public int AddItem(ItemSO item, int quantity, List<ItemParameter> itemState = null)
        {
            if(item.IsStackable == false)
            {
                for (int i = 0; i < inventoryItems.Count; i++)
                {
                    while(quantity > 0 && IsInventoryFull() == false)
                    {
                        quantity -= AddItemToFirstFreeSlot(item, 1, itemState);
                    }
                    InformAboutChange();
                    return quantity;
                }
            }
            quantity = AddStackableItem(item, quantity);
            InformAboutChange();
            return quantity;
        }

        public void RemoveItem(int itemIndex, int amount)
        {
            if (inventoryItems.Count > itemIndex)
            {
                if (inventoryItems[itemIndex].IsEmpty)
                    return;
                int reminder = inventoryItems[itemIndex].quantity - amount;
                if (reminder <= 0)
                    inventoryItems[itemIndex] = InventoryItem.GetEmptyItem();
                else
                    inventoryItems[itemIndex] = inventoryItems[itemIndex]
                        .ChangeQuantity(reminder);

                InformAboutChange();
            }
        }

        public void AddItem(InventoryItem item)
        {
            AddItem(item.item, item.quantity);
        }

        public Dictionary<int, InventoryItem> GetCurrentInventoryState()
        {
            Dictionary<int, InventoryItem> returnValue =
                new Dictionary<int, InventoryItem>();

            for (int i = 0; i < inventoryItems.Count; i++)
            {
                if (inventoryItems[i].IsEmpty)
                    continue;
                returnValue[i] = inventoryItems[i];
            }
            return returnValue;
        }

        public InventoryItem GetItemAt(int itemIndex)
        {
            return inventoryItems[itemIndex];
        }

        public void SwapItems(int itemIndex_1, int itemIndex_2)
        {
            InventoryItem item1 = inventoryItems[itemIndex_1];
            inventoryItems[itemIndex_1] = inventoryItems[itemIndex_2];
            inventoryItems[itemIndex_2] = item1;
            InformAboutChange();
        }

        private void InformAboutChange()
        {
            OnInventoryUpdated?.Invoke(GetCurrentInventoryState());
        }


    }

    [Serializable]
    public struct InventoryItem
    {
        //public int ID;
        public int quantity;
        public ItemSO item;
        public List<ItemParameter> itemState;
        public bool IsEmpty => item == null;

        public InventoryItem ChangeQuantity(int newQuantity)
        {
            return new InventoryItem
            {
                item = this.item,
                quantity = newQuantity,
               itemState = new List<ItemParameter>(this.itemState)
            };
        }

        public static InventoryItem GetEmptyItem()
            => new InventoryItem
            {
                item = null,
                quantity = 0,
                itemState = new List<ItemParameter>()
            };
    }
}
My ItemSO script:

Code: Select all

 namespace Inventory.Model
 {
    public abstract class ItemSO : ScriptableObject
    {
        [field: SerializeField]
        public bool IsStackable { get; set; }

        public int ID => GetInstanceID();

        [field: SerializeField]
        public int MaxStackSize { get; set; } = 1;

        [field: SerializeField]
        public string Name { get; set; }

        [field: SerializeField]
        [field: TextArea]
        public string Description { get; set; }

        [field: SerializeField]
        public Sprite ItemImage { get; set; }

        [field: SerializeField]
        public List<ItemParameter> DefaultParametersList { get; set; }


    }

    [Serializable]
    public struct ItemParameter : IEquatable<ItemParameter>
    {
        public ItemParameterSO itemParameter;
        public float value;

        public bool Equals(ItemParameter other)
        {
            return other.itemParameter == itemParameter;
        }
    }

}
User avatar
Tony Li
Posts: 21684
Joined: Thu Jul 18, 2013 1:27 pm

Re: Saving Inventory Between Scenes (Scriptable Objects)

Post by Tony Li »

Hi,

There was a small typo in the tutorial. "item" should have been "items".

In your case, instead of this:

Code: Select all

foreach (var item in inventory.item)
try this:

Code: Select all

foreach (var item in inventory.inventoryItems)
DrewThomasArt
Posts: 60
Joined: Thu Mar 24, 2022 12:07 am

Re: Saving Inventory Between Scenes (Scriptable Objects)

Post by DrewThomasArt »

Tony Li wrote: Thu Apr 13, 2023 8:06 am
Thank you, RecordData() is now functioning,

This is my ApplyData() method:

Code: Select all

public void ApplyData(string s)
{
    if (string.IsNullOrEmpty(s)) return; // If this isn't valid save data, skip.
    var inventory = GetComponent<InventorySO>();
    var saveData = SaveSystem.Deserialize<InventorySaveData>(s);
    if (saveData == null) return; // If we fail to deserialize it, skip.
    inventory.inventoryItems = new List<InventoryItem>();
    foreach (var itemName in saveData.itemNames)
    {
        var item = Resources.Load<InventoryItem>(itemName);
        inventory.inventoryItems.Add(item);
    }
}
I'm getting this error:
The type 'Inventory.Model.InventoryItem' cannot be used as type parameter 'T' in the generic type or method 'Resources.Load<T>(string)'. There is no boxing conversion from 'Inventory.Model.InventoryItem' to 'UnityEngine.Object'.

Could this be another simple issue or do I just have to figure that out?
When I replace, InventoryItem with ItemSO, I get an error telling me I can't convert between the two.
Cannot implicitly convert type 'System.Collections.Generic.List<Inventory.Model.ItemSO>' to 'System.Collections.Generic.List<Inventory.Model.InventoryItem>'

Thanks again for any help!
User avatar
Tony Li
Posts: 21684
Joined: Thu Jul 18, 2013 1:27 pm

Re: Saving Inventory Between Scenes (Scriptable Objects)

Post by Tony Li »

The example code is meant to be a model, not play-ready code. You'll need to fill in some things based on your specific inventory implementation. There are at least two things you may want to change:

Change this line:

Code: Select all

var inventory = GetComponent<InventorySO>();
to:

Code: Select all

var inventory = GetComponent<Inventory>();
assuming you have a MonoBehaviour script named Inventory. You can't use GetComponent<> for ScriptableObject assets.

Then change this line:

Code: Select all

var item = Resources.Load<InventoryItem>(itemName);
to something like:

Code: Select all

var itemSO = Resources.Load<ItemSO>(itemName);
var inventoryItem = new InventoryItem() { item = itemSO; };
inventory.inventoryItems.Add(inventoryItem);
In this case, you do want to load the ItemSO asset, and then create an InventoryItem object to add to your inventory.inventoryItems list that points to the ItemSO asset.
DrewThomasArt
Posts: 60
Joined: Thu Mar 24, 2022 12:07 am

Re: Saving Inventory Between Scenes (Scriptable Objects)

Post by DrewThomasArt »

Tony Li wrote: Thu Apr 13, 2023 10:52 am The example code is meant to be a model, not play-ready code. You'll need to fill in some things based on your specific inventory implementation. There are at least two things you may want to change:

Change this line:

Code: Select all

var inventory = GetComponent<InventorySO>();
to:

Code: Select all

var inventory = GetComponent<Inventory>();
assuming you have a MonoBehaviour script named Inventory. You can't use GetComponent<> for ScriptableObject assets.

Then change this line:

Code: Select all

var item = Resources.Load<InventoryItem>(itemName);
to something like:

Code: Select all

var itemSO = Resources.Load<ItemSO>(itemName);
var inventoryItem = new InventoryItem() { item = itemSO; };

inventory.inventoryItems.Add(inventoryItem);
In this case, you do want to load the ItemSO asset, and then create an InventoryItem object to add to your inventory.inventoryItems list that points to the ItemSO asset.
Thank you for your response, I'll make it work from here!
User avatar
Tony Li
Posts: 21684
Joined: Thu Jul 18, 2013 1:27 pm

Re: Saving Inventory Between Scenes (Scriptable Objects)

Post by Tony Li »

Glad to help!
Post Reply