Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice

Creating an Inventory System with Scriptable Objects

Discussion in 'Scripting' started by yepfuk, Sep 21, 2016.

  1. yepfuk

    yepfuk

    Joined:
    Sep 23, 2015
    Posts:
    67
    Hi,

    I want to create an Inventory System and I want to use Scriptable Objects for Items. I create base InventoryItem class derive from ScriptableObject. And I create all other items derive from InventoryItem class. Like this;


    Code (CSharp):
    1. public class InventoryItem : ScriptableObject
    2. {
    3.        public int itemID;
    4.        public string itemName;
    5.        public ItemType itemType;
    6.        // etc...
    7. }
    8.  
    9. public class WeaponItem : InventoryItem
    10. {
    11.         public float weaponBaseDamage;
    12.         public float weaponCriticalHitChance;
    13.         // etc...
    14. }
    15.  
    16. [CreateAssetMenu(fileName = "InventoryItems", menuName = "RangedWeapon")]
    17. public class RangedWeapon : WeaponItem
    18. {
    19.       public float weaponBaseReloadSpeed;
    20.       public float weaponBaseFireRate;
    21.       // etc...
    22. }
    23.  
    But the problem is, how can I merge all this items together in one List for my item database?
    I tried to use interface(IInventoriable) but I think this is not the solution.

    Is there any approach or design pattern to manage this kind of inventory system or should I re-design all of that? ( I heard about composition over inheritence approach for inventory systems but I don't know is this the right approach to solve this kind of problem.)

    Thank you...
     
    studiowebux likes this.
  2. LiterallyJeff

    LiterallyJeff

    Joined:
    Jan 21, 2015
    Posts:
    2,802
    You can keep a List of InventoryItem which all items can be in. But you'll have to cast to the different types in order to get the derived class-specific data. If your ItemType contains what kind it is, then you can switch on that and cast to the appropriate type I suppose.
     
    yepfuk likes this.
  3. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,184
    As far as I know, this will not work, when trying to serialize such a list in the editor. It will work to drag different InventoryItems child classes into the list, but once they are serialized and deserialized, the child class information is lost, because only the base class data will be stored. It's a limitation of the serialization system. Correct me if I'm wrong, since I was struggling with that problem, too.
     
    yepfuk and LiterallyJeff like this.
  4. LiterallyJeff

    LiterallyJeff

    Joined:
    Jan 21, 2015
    Posts:
    2,802
    Edit:
    Corrected below
     
    Last edited: Sep 21, 2016
  5. yepfuk

    yepfuk

    Joined:
    Sep 23, 2015
    Posts:
    67
    You mean like this;


    Code (CSharp):
    1.  
    2.  
    3. public List<InventoryItem> allItems = new List<InventoryItem>();
    4.  
    5. private void Start()
    6. {
    7.            for(int i=0; i < allItems.Count; i++)
    8.            {
    9.                     if(allItems[i].itemType == InventoryItem.ItemType.Wearable)
    10.                    {
    11.                          WearableItem wItem = allItems[i] as WearableItem;
    12.                    }
    13.                    // etc...
    14.           }
    15. }
    16.  
     
    studiowebux likes this.
  6. LiterallyJeff

    LiterallyJeff

    Joined:
    Jan 21, 2015
    Posts:
    2,802
    Edit:
    Corrected below
     
    Last edited: Sep 21, 2016
    yepfuk likes this.
  7. NickAtUnity

    NickAtUnity

    Unity Technologies

    Joined:
    Sep 13, 2016
    Posts:
    84
    You can absolutely make a list of the base type and it will serialize fine. When you have a list to reference types that derive from ScriptableObject, all the list is serializing is a list of references; not the data itself. As an example, I took the basics of your objects above and then made two assets in my project: a ranged weapon, and a simple inventory where the inventory is just this:

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. [CreateAssetMenu]
    4. public class Inventory : ScriptableObject
    5. {
    6.     public InventoryItem[] items;
    7. }
    I then used the Editor Settings to force assets to serialize to text to easily introspect them. You can see that the weapon asset contains all the data I changed in the editor:

    Code (CSharp):
    1. %YAML 1.1
    2. %TAG !u! tag:unity3d.com,2011:
    3. --- !u!114 &11400000
    4. MonoBehaviour:
    5.   m_ObjectHideFlags: 0
    6.   m_PrefabParentObject: {fileID: 0}
    7.   m_PrefabInternal: {fileID: 0}
    8.   m_GameObject: {fileID: 0}
    9.   m_Enabled: 1
    10.   m_EditorHideFlags: 0
    11.   m_Script: {fileID: 11500000, guid: 64fdcd8cc1c9b0345903545385500db3, type: 3}
    12.   m_Name: RangedWeapon1
    13.   m_EditorClassIdentifier:
    14.   itemID: 123
    15.   itemName: My Cool Bow
    16.   weaponBaseDamage: 34
    17.   weaponCriticalHitChance: 45
    18.   weaponBaseReloadSpeed: 2
    19.   weaponBaseFireRate: 1
    20.  
    Meanwhile all the inventory stores is just a reference to that file:

    Code (CSharp):
    1. %YAML 1.1
    2. %TAG !u! tag:unity3d.com,2011:
    3. --- !u!114 &11400000
    4. MonoBehaviour:
    5.   m_ObjectHideFlags: 0
    6.   m_PrefabParentObject: {fileID: 0}
    7.   m_PrefabInternal: {fileID: 0}
    8.   m_GameObject: {fileID: 0}
    9.   m_Enabled: 1
    10.   m_EditorHideFlags: 0
    11.   m_Script: {fileID: 11500000, guid: 9681b29632ae42143bdab1c449658037, type: 3}
    12.   m_Name: Inventory
    13.   m_EditorClassIdentifier:
    14.   items:
    15.   - {fileID: 11400000, guid: f6d28ba781f955b4e955e0c045dc7a68, type: 2}
    16.  
    I also made a simple script that prints out the inventory and shows that you can cast to a specific type to get the fields:

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class RuntimeCheck : MonoBehaviour {
    4.  
    5.     public Inventory inventory;
    6.  
    7.     void Start () {
    8.         foreach (var item in inventory.items) {
    9.             Debug.Log(item.GetType().Name);
    10.  
    11.             var rangedWeapon = item as RangedWeaponItem;
    12.             if (rangedWeapon)
    13.             {
    14.                 Debug.Log(rangedWeapon.weaponBaseReloadSpeed);
    15.             }
    16.         }
    17.     }
    18. }
    19.  
    Note I tested with 5.4.1f1 so earlier versions of Unity may have different results.
     
    studiowebux, yepfuk and LiterallyJeff like this.
  8. LiterallyJeff

    LiterallyJeff

    Joined:
    Jan 21, 2015
    Posts:
    2,802
    Thank you so much for clearing this up. I did some research earlier and must have found old or incorrect information about this. I'm very glad it works the way I originally expected.
     
    yepfuk likes this.
  9. yepfuk

    yepfuk

    Joined:
    Sep 23, 2015
    Posts:
    67
    Thank you all guys, much appreciated.
     
  10. bitbiome_llc

    bitbiome_llc

    Joined:
    Aug 3, 2015
    Posts:
    58
    How do you save changes to the inventory at runtime? I have a similar setup. But it is useless (as far as I can tell) at runtime. I cannot save the inventory changes to disk because it errors out due to the scriptableobject. I've been trying to merge the ease of using scriptableobjects in the editor and the ability to change and save those changes at runtime.... but I have not had any luck.

    For example. If we were to take this same example. Add an item to the inventory at runtime and then try to save it disk for later use you get the following error:

    SerializationException: Type UnityEngine.ScriptableObject in assembly UnityEngine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null is not marked as serializable.

    Are scriptableobjects useful in a game where you want to save the state? If so, how do you get both the ease of making items in the editor as scriptableobjects but also be able to save the state of any instances of those objects at runtime?
     
    Last edited: Oct 1, 2016
    studiowebux likes this.
  11. NickAtUnity

    NickAtUnity

    Unity Technologies

    Joined:
    Sep 13, 2016
    Posts:
    84
    A great resource on ScriptableObject is this talk by Unity engineer Richard Fine. It covers a lot of cool things you can use ScriptableObjects for. In it, he makes a mention to using JsonUtility to override properties of the data from a JSON file he saves out. This lets him have default values (setup in the editor) but can then save and load values from JSON. I believe he uses it to store/load the default player list for his four-player version of the tank game near the end.

    That said, having the class use BinaryWriter and BinaryReader to save/load its values to disk is another approach that would work well. Custom serialization can be incredibly useful when you're saving player critical data (progress, inventory, etc) and want to have the ability to more easily ensure backward compatibility in case you need to modify the format.
     
  12. bitbiome_llc

    bitbiome_llc

    Joined:
    Aug 3, 2015
    Posts:
    58
    Thanks for the reply. I had seen some scriptableobject talks but not this specific one. I was already leaning toward the jsonutility but was having issues figuring out when it saves the entire object vs. an instance like the example below. I think I'm on the right track and adjusting which classes inherit from scriptableobject and which do not.

    Code (JavaScript):
    1. {
    2.     "name":"Steve",
    3.     "handRight"={
    4.         "name"="Sword",
    5.         "damage"=10
    6.     }
    7. }
    vs.

    Code (JavaScript):
    1. {
    2.     "name":"Steve",
    3.     "handRight"={
    4.         {"instanceID":12780}
    5.     }
    6. }
     
  13. bitbiome_llc

    bitbiome_llc

    Joined:
    Aug 3, 2015
    Posts:
    58
    That video had some nice insights but I still feel like I'm approaching it wrong or maybe backwards?

    I'd like to have a character as a scriptableobject. This is used as a starting point for a character. It allows me to drop characters into a scene in the editor or on the fly in game. Currently when I add a character to the scene I create an instance of it so I can tweak it in the editor without affecting the base scriptableobject.

    My issue arrives when I try to save all the characters that are on the map. If I stuff the characters into a list on a save game object and use toJson I get instanceID's to those character objects. This method only saves the instance to the character. I will lose my characters this way.

    Code (CSharp):
    1. [Serializable]
    2. public class GameSave {
    3.  
    4.     public string datetime;  
    5.     public List<Character> characters = new List<Character>();
    6.      
    7.     public void SaveGame(string path) {
    8.  
    9.         //grab all active characters on map
    10.         foreach( Character liveChar in CharacterManager.All() )
    11.         {
    12.             characters.Add(liveChar);
    13.         }
    14.      
    15.         System.IO.File.WriteAllText(path, JsonUtility.ToJson(this, true));  
    16.     }
    17. }
    18.  
    19. [Serializable]
    20. public class Character : ScriptableObject
    21. {
    22.         public string charName;
    23.         public string description;
    24.      
    25.         public Sprite unitSprite;
    26. }
    27.  
    28. JSON File
    29. {
    30.     "datetime":"",
    31.     "characters": [{instanceid:23948}]
    32. }
    33.  
    I need the character as a json object with all the fields. If I convert each character to json and then save the game save object it does save all the fields but as a string and this creates a lot of escape characters. Is there a more correct way to do this?

    Is there a method to save object fields instead of the object instance? If I convert each object to json and stuff it in an array it works but there are a crazy amount of escape characters. Or is that just how it works right now?

    Code (CSharp):
    1. [Serializable]
    2. public class GameSave {
    3.  
    4.     public string datetime;
    5.     public List<string> characterJsons = new List<string>();
    6.      
    7.     public void SaveGame(string path) {
    8.  
    9.         //grab all active characters on map
    10.         foreach( Character liveChar in CharacterManager.All() )
    11.         {
    12.             characterJsons.Add( JsonUtility.ToJson(liveChar,true) );
    13.         }
    14.      
    15.         System.IO.File.WriteAllText(path, JsonUtility.ToJson(this, true));  
    16.     }
    17. }
    18.  
    19. [Serializable]
    20. public class Character : ScriptableObject
    21. {
    22.         public string charName;
    23.         public string description;
    24.      
    25.         public Sprite unitSprite;
    26. }
    27.  
    28.  
    29. JSON File
    30.  
    31. {
    32.     "datetime":"",
    33.     "characterJsons": ["{\"charName\":\"bob\",\"description\":\"hi bob\",\"unitSprite\":{\"instanceID\":16694}}"]
    34. }
    35.  
    36.  
    37.  
     
  14. yepfuk

    yepfuk

    Joined:
    Sep 23, 2015
    Posts:
    67
    What are you mean "escape characters" ?

    Another way to save your characters is when you save the game take all your characters' ID's that you want to save. And put those ID's in a list then save this list. When you want to load your characters get this saved ID's and then get all characters in your Character Database matches that saved ID's.
    All you have to do is create a Character Database of your Character objects.
     
  15. bitbiome_llc

    bitbiome_llc

    Joined:
    Aug 3, 2015
    Posts:
    58
    The issue is saving the character list during gameplay at runtime. Nothing to do with the editor, saving content in the editor is already done and functional.

    By escape character I mean the \ that comments out the " in the sub json object.

    If the characters in the list are scriptableobjects and you try to use binarywriter you get an error because the scriptableobject class cannot be serialized outside the editor environment. Which is why I was leaning toward json.

    I'd love to see an example of how someone has used a scriptableobject as a template for a character and item, added an instance of that object to the scene, run the game, edit that object during gameplay, and then save that character with a list of items (inventory) during gameplay. I have all but the last step working while retaining ease of use in the editor.

    Almost all the examples I see just reset all the objects when exiting play mode. I get saving a simple relatively "flat" object with toJson and writing it do disk with a string, but I don't see how it works if your base template object is a scriptableobject and your character has an inventory of more scriptableobjects.

    Obviously one way of doing that is to save the object key from the dictionary the object is within that is created when the game launches. But that breaks the ease of use in the editor for adding items to a character inventory. That is, if I view the character inventory in the editor I see a bunch of keys instead a reference to the scriptableobject itself.

    I've been unable to keep a intuitive workflow in the editor and be able to save out my character with an inventory. This makes believe I am not using this as the Unity development team intended. If I save keys I lose the ease of setting up items in a character inventory in the editor. If I keep the ability to drag and drop a scriptableobject into an a character inventory I'm unable to find a way (or example) of how to save that character and inventory.
     
    Last edited: Oct 3, 2016
  16. bitbiome_llc

    bitbiome_llc

    Joined:
    Aug 3, 2015
    Posts:
    58
    After some testing I settled on the XmlSerializer.

    This allows the use of a scriptableobject for ease of use in the editor but also allows me to flag fields I do not want to save to disk. This was my main issue. I wanted the field to be serialized by the editor but not at runtime, yet still visible and editable in the editor.

    Another issue was the GearSlot list and gear object on a character. Using the built in Unity toJson results in a instance string {instance:32434}. While I could convert each to its own json string that process gets a bit ugly. Maybe there is another method, but I jumped ship at this point.

    The XmlSerializer will serialize all the item fields and properties into a xml file which is a bit too much and of course throws errors for most of the custom Unity classes. Placing XmlIgnore on a few fields fixed that behavior. Finally the XmlSerializer has no deserialize callback, so I setup a property for the itemRef which is used to pull up the correct scriptableobject at runtime. All the items in the game are held in a scriptableobject list that creates a reference dictionary at runtime.

    I'm sure there are some issues or holes in this setup but it works. This allows for saving fairly complex classes to disk, requires minimal work (no need redefine/rewrite code for everything using IXmlSerializable), and still allows use of all the needed fields in the editor itself. Hopefully this will help someone in the future.

    Code (CSharp):
    1. [Serializable]
    2. public class Character : ScriptableObject
    3. {
    4.     public string portraitSpriteName;
    5.     public string unitSpriteName;
    6.  
    7.     [XmlIgnore]
    8.     public Sprite unitSprite;
    9.     [XmlIgnore]
    10.     public Sprite portraitSprite;
    11.  
    12.     //Character GearSlots, just a class for specific gear slots (head, chest, pants, etc)
    13.     public CharacterGear gear;
    14.  
    15.     //character inventory, a list of GearSlots
    16.     public List<GearSlot> inventory;
    17. }
    18.  
    19. [Serializable]
    20. public class GearSlot : ISerializationCallbackReceiver
    21. {
    22.     public int count;
    23.  
    24.     //allows use in Unity Editor
    25.     [XmlIgnore]
    26.     public string _itemRef;
    27.  
    28.     //Still saves in XmlSerializer
    29.     public string itemRef {
    30.         get{
    31.             return _itemRef;
    32.         }
    33.         set{
    34.             _itemRef = value;
    35.             OnAfterDeserialize();
    36.         }
    37.     }
    38.  
    39.     //this is a scriptable object for use in Unity
    40.     [XmlIgnore]
    41.     public Item item;
    42.  
    43.     //Unity event and sets object on xml deserialize
    44.     public void OnAfterDeserialize ()
    45.     {
    46.         if( item == null && !string.IsNullOrEmpty(itemRef) )
    47.             item = ItemManager.GetItem(itemRef);
    48.     }
    49. }