Search Unity

Inventory save system with dynamic items

Discussion in 'Scripting' started by Magnilum, Apr 21, 2021.

  1. Magnilum

    Magnilum

    Joined:
    Jul 1, 2019
    Posts:
    241
    Hi, I am making a game and I have been stuck for a while with this, so, here is my problem.

    To start, I have made a Item class (ScriptableObject):

    Code (CSharp):
    1. public class Item : ScriptableObject {
    2.    
    3.     public new string name;
    4.     public GameObject slotUI;
    5.     public GameObject infoUI;
    6.     public Sprite sprite;
    7.     public float size;
    8.     public bool isStackable;
    9.     [TextArea] public string description;
    10.  
    11. }
    In order to store the amount of items in the player inventory, I have made a second class which store the item and the amount of item:

    Code (CSharp):
    1. public class ItemContent
    2. {
    3.     public Item item;
    4.     public int amount;
    5.    
    6.     public Action OnChanged;
    7.  
    8.     public ItemContent(Item item, int count)
    9.     {
    10.         this.item = item;
    11.         SetAmount(count);
    12.     }
    13.  
    14.     public void Add(int amount)
    15.     {
    16.         this.amount += amount;
    17.         Update();
    18.     }
    19.  
    20.     public void Remove(int amount)
    21.     {
    22.         this.amount -= amount;
    23.  
    24.         if (amount < 0)
    25.             this.amount = 0;
    26.  
    27.         Update();
    28.     }
    29.  
    30.     public bool RemoveCheck(int amount)
    31.     {
    32.         if (this.amount - amount >= 0)
    33.         {
    34.             this.amount -= amount;
    35.             Update();
    36.             return true;
    37.         }
    38.         return false;
    39.     }
    40.  
    41.     public void SetAmount(int amount)
    42.     {
    43.         this.amount = amount;
    44.         Update();
    45.     }
    46.  
    47.     void Update()
    48.     {
    49.         OnChanged?.Invoke();
    50.     }
    51. }
    And this is the inventory player class:

    Code (CSharp):
    1. public class Inventory
    2. {
    3.     public float maxSize;
    4.  
    5.     List<ItemContent> content = new List<ItemContent>();
    6.  
    7.     public Action<ItemContent> OnItemAdded;
    8.     public Action<ItemContent> OnItemRemoved;
    9.     public Action OnItemAmountChanged;
    10.  
    11.     public bool Add(Item item, int count)
    12.     {
    13.         if (CanAdd(item, count))
    14.         {
    15.             ItemContent itemContent = GetItemContent(item);
    16.  
    17.             if (itemContent != null && item.isStackable)
    18.                 itemContent.Add(count);
    19.             else
    20.             {
    21.                 itemContent = new ItemContent(item, count);
    22.                 content.Add(itemContent);
    23.  
    24.                 OnItemAdded?.Invoke(itemContent);
    25.             }
    26.  
    27.             OnItemAmountChanged?.Invoke();
    28.  
    29.             return true;
    30.         }
    31.         else
    32.         {
    33.             Debug.Log("Inventory is full !");
    34.             return false;
    35.         }
    36.     }
    37.  
    38.     public bool Remove(Item item, int count)
    39.     {
    40.         ItemContent itemContent = GetItemContent(item);
    41.  
    42.         if (itemContent != null)
    43.         {
    44.             if (itemContent.RemoveCheck(count))
    45.             {
    46.                 if (itemContent.amount <= 0)
    47.                 {
    48.                     content.Remove(itemContent);
    49.  
    50.                     OnItemRemoved?.Invoke(itemContent);
    51.                 }
    52.  
    53.                 OnItemAmountChanged?.Invoke();
    54.  
    55.                 return true;
    56.             }
    57.         }
    58.         return false;
    59.     }
    60.  
    61.     public ItemContent GetItemContent(Item item)
    62.     {
    63.         foreach (var itemContent in content)
    64.             if (itemContent.item.ID == item.ID)
    65.                 return itemContent;
    66.         return null;
    67.     }
    68.  
    69.     public bool CanAdd(Item item, int count)
    70.     {
    71.         return (GetCurrentSize() + (item.size * count)) <= maxSize;
    72.     }
    73.  
    74.     public float GetCurrentSize()
    75.     {
    76.         float size = 0f;
    77.         foreach (var itemContent in content)
    78.             size += itemContent.item.size * itemContent.amount;
    79.         return size;
    80.     }
    81. }
    It may exist a better way to do that but throught all tutorials I saw, I guess it's one of the best way.

    It is now that problems come. In order to save the inventory between two sessions, I use Json format. I know I have to create somes classes like "InventorySave" and "ItemContentSave" which register the player inventory with the ID of each item and its amount. It's not very difficult but some of my items like swords has an attack damage and the player can improve the stats of each one. But the modification must only be for the sword he has into his inventory and not for all swords which share the same ScriptableObject and could be present on enemies. Moreover, to damage the enemy, I would like to use collision between the weapon prefab and the enemy and apply damage to it so I use OnTriggerEnter method for that.

    Code (CSharp):
    1.  
    2. public class Weapon : Item {
    3.  
    4.     [Header("Weapon")]
    5.     public HandType handType;
    6.     public AnimatorOverrideController animationOverrider;
    7.  
    8.     [Header("Damage values")]
    9.     public float damage;
    10. }
    11.  
    The only way to modify the damage value without changing default value for the other sword is to instantiate the item. But, if I instantiate the item, when I reload my scene, the item in the inventory is no longer the instantiated item (clone) modified but the basic one with the value of my ScriptableObject which is normal.

    To resume, I am looking for a way to use ScriptableObject to store data that will not be modified like the name, the size, the prefab, the sprite icon ... and use a class attach to the prefab to store data relative to this object and only this one.

    I stated to write something on it and this is what I have.

    This is the ScriptableObject which stores all static data:

    Code (CSharp):
    1. public class ItemSO : ScriptableObject {
    2.  
    3.     public string ID;
    4.  
    5.     [Header("Item")]
    6.     public new string name;
    7.     public InventoryType inventoryType;
    8.     public GameObject prefab;
    9.     public GameObject slotUI;
    10.     public GameObject infoUI;
    11.     public Sprite sprite;
    12.     public float weight;
    13.     public bool isStackable;
    14.     [TextArea] public string description;
    15.  
    16.     void OnValidate()
    17.     {
    18.         ID = inventoryType.ToString().ToLower() + ":" + name.ToLower().Replace(" ", "_");
    19.     }
    20. }
    21.  
    22. public class WeaponSO : ItemSO {
    23.  
    24.     [Header("Weapon")]
    25.     public HandType handType;
    26.     public AnimatorOverrideController animationOverrider;
    27.  
    28. }
    This is the class attach to the prefab which store dynamic data:

    Code (CSharp):
    1. public class WeaponObject : MonoBehaviour {
    2.  
    3.     [Header("Durability")]
    4.     public float durability;
    5.  
    6.     [Header("Damage values")]
    7.     public float damage;
    8.  
    9.     protected void OnTriggerEnter(Collider other)
    10.     {
    11.         Entity entityHit = other.gameObject.GetComponent<Entity>();
    12.  
    13.         if (entityHit != null)
    14.         {
    15.             entityHit.TakeDamage(damage);
    16.         }
    17.     }
    18. }
    With this, I need to instantiate each item whereas some of them needn't to be like wheat, key or more. So I have to instantiate all of them ans manage them with their prefab which will be a mess of code ? Or there is a better way ?
     
  2. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    I would invert the relationship you have now, where SOs reference prefabs. Instead, treat the scriptable objects as the blueprints for the equivalent concrete, instantiated item. Then add on special data as necessary.

    Code (csharp):
    1.  
    2. class ItemBlueprint : ScriptableObject
    3. {
    4.    // all the data you have
    5. }
    6.  
    7. class WeaponBlueprint : ItemBlueprint
    8. {
    9.    int baseDamage;
    10. }
    11.  
    12. class Weapon : MonoBehaviour
    13. {
    14.    WeaponBlueprint blueprint;
    15.  
    16.    int bonusDamage;
    17.  
    18.    public int damage { get { return blueprint.baseDamage + bonusDamage; } }
    19. }
    20.  
    This way you only need to manage the instantiated items in your serialization and other game systems, like inventory.
     
    Stoicheia likes this.
  3. Magnilum

    Magnilum

    Joined:
    Jul 1, 2019
    Posts:
    241
    Thank you for your answer, I thought to this solution but I didn't know if it was a good idea or not, now I will try. But before just to be sure, so in my inventory I should put the Weapon Monobehaviour into the ItemContent to register the amount of an item ?
    And to save those data in Json, do you have an idea ? Because I can save ItemContent and inventory whitout too many problems but when I save the ID and the Amount, the Item must be Instantiate, I can do that with getting the prefab from the database with the ID and how can I get back modifications ?
     
  4. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    Saving and loading your game data is going to be highly tailored to your game. I'm not sure why you can't save the weapon though, the bonus damage and the blueprint it's referencing along with any other unique data.
     
  5. Magnilum

    Magnilum

    Joined:
    Jul 1, 2019
    Posts:
    241
    I setup what you said to better explain. This what I have done.

    To start I made an ItemSpanwer which spawn the item.

    Code (CSharp):
    1. public class ItemSpawner : MonoBehaviour {
    2.  
    3.     public ItemObject itemObject;
    4.  
    5.     void Start()
    6.     {
    7.         itemObject = Instantiate(itemObject, transform);
    8.     }
    9. }
    Then I update my classes to be as yours.

    Code (CSharp):
    1. public class ItemSO : ScriptableObject {
    2.  
    3.     public new string name;
    4.     public InventoryType inventoryType;
    5.     public GameObject slotUI;
    6.     public GameObject infoUI;
    7.     public Sprite sprite;
    8.     public float weight;
    9.     public bool isStackable;
    10.     [TextArea] public string description;
    11.  
    12. }
    13.  
    14. public class WeaponSO : ItemSO {
    15.  
    16.     [Header("Weapon")]
    17.     public HandType handType;
    18.     public AnimatorOverrideController animationOverrider;
    19.  
    20.     [Header("Damage")]
    21.     public float baseDamage;
    22.  
    23. }
    24.  
    25. public class WeaponObject : ItemObject {
    26.  
    27.     [Header("Blueprint")]
    28.     public WeaponSO blueprint;
    29.  
    30.     [Header("Durability")]
    31.     public float durability;
    32.  
    33.     float bonusDamage;
    34.  
    35.     public float damage
    36.     {
    37.         get
    38.         {
    39.             return blueprint.baseDamage + bonusDamage;
    40.         }
    41.     }
    42.  
    43.     void Start()
    44.     {
    45.         bonusDamage += 10f;
    46.     }
    47.  
    48.     // SAVE SYSTEM
    49.  
    50.     void OnApplicationQuit()
    51.     {
    52.         Save();
    53.     }
    54.  
    55.     void Save()
    56.     {
    57.         WeaponObjectSave weaponObjectSave = new WeaponObjectSave()
    58.         {
    59.             position = transform.localPosition,
    60.             rotation = transform.rotation.eulerAngles,
    61.             bonusDamage = bonusDamage
    62.         };
    63.  
    64.         weaponObjectSave.Save(gameObject.name);
    65.     }
    66.  
    67.     public class WeaponObjectSave
    68.     {
    69.         public Vector3 position;
    70.         public Vector3 rotation;
    71.         public float bonusDamage;
    72.  
    73.         public virtual void Save(string fileName)
    74.         {
    75.             string json = JsonUtility.ToJson(this, true);
    76.             SaveSystem.instance.SaveJson(fileName, json);
    77.         }
    78.     }
    79. }
    And the mace prefab looks like that:

    (image 1)​

    When I start the game, the spawner works well and I have this:

    (image 2)​

    In the inspector for the Weapon_Mace(Clone), I have this:

    (image 3)​

    So the bonusDamage is 10 as expected due to the line 45 in the WeaponObject.

    When the application exit, I get this Json file:

    (image 4)​

    So I correctly save the bonusDamage in the file. But now I am stuck with the loading system. Cause how to say to the Weapon_Mace(Clone) to load this particular file and not an other one which could correspond to another Weapon_Mace(Clone) in the world. I could use guid generated each time the WeaponObject is Instantiated but the weapon saved will have a guid different from the one which will be instantiated for the second session.
     

    Attached Files:

  6. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    Yep, you'll need to come up with your own guid/id system to re-establish references between objects as well as some bookkeeping system to find the SOs.
     
  7. Magnilum

    Magnilum

    Joined:
    Jul 1, 2019
    Posts:
    241
    I don't know if it's possible, it should exist an other way to do that by this is not easy to find.
     
  8. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    I personally use a System.Guid. Here's a snippet from my base actor component:
    Code (csharp):
    1.  
    2. using System;
    3.  
    4. public abstract class ActorComponent : ProgenitorComponent
    5. {
    6.    protected Guid _guid;
    7.  
    8.    public Guid guid { get { return _guid; } }
    9.  
    10.    protected override void Awake()
    11.    {
    12.        base.Awake();
    13.  
    14.        _guid = GenerateGuid();
    15.    }
    16.  
    17.    public static Guid GenerateGuid()
    18.    {
    19.        return Guid.NewGuid();
    20.    }
    21. }
    22.  
     
  9. Magnilum

    Magnilum

    Joined:
    Jul 1, 2019
    Posts:
    241
    I would say, an other way to save Instantiated Object and get back them on loading.
     
  10. Magnilum

    Magnilum

    Joined:
    Jul 1, 2019
    Posts:
    241
    Guid are very useful for gameobject which are not instantiated in running time, isn't it ? It's possible to put un guid on each gameobject, save this guid and when loading, get the file with the correct guid and load data.

    But for gameobjects which are instantiated in play mode, is it possible to generate a guid and keep it between 2 sessions ?
     
  11. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    Of course, you can set data to whatever you want on load.
     
  12. Magnilum

    Magnilum

    Joined:
    Jul 1, 2019
    Posts:
    241
    I keep going to solve this problem or find something else, thank you ;)