Search Unity

[SOLVED] ScriptableObject Databases Lose List Data on "Play"

Discussion in 'Immediate Mode GUI (IMGUI)' started by DonLoquacious, Aug 1, 2015.

  1. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    Having a bit of difficulty with my modified implementation of Inventory Master from the asset store. I decided to try and mix a little inheritance into the process, and everything went well right up until I noticed that the save data (particularly the list data) doesn't seem to "stick" after play is pressed or Unity restarted. There's some sort of serialization problem that I just can't figure out. Here's how I've set things up:

    First, I have my item classes, abstract base class "item" with all sorts of little bits and pieces, and several classes that derive from it with one or two additions each.

    Code (csharp):
    1. namespace Lysander.Items
    2. {
    3.     [System.Serializable]
    4.     public abstract class Item
    5.     {
    6.         public string itemName;
    7.         public int itemID;
    8.         //ETC
    9.     }
    10.  
    11.     [System.Serializable]
    12.     public class Weapon : Item
    13.     {
    14.         public WeaponType weaponType = WeaponType.None;
    15.         //ETC
    16.     }
    17.     //
    18.     //ETC
    19.     //
    20. }
    You get the idea. I then have my scriptable object class, which looks something like this (you'll notice the list data is for the abstract base class "item", and not the derived classes that actually get held in the list- this may be relevant to the problem):

    Code (csharp):
    1. namespace Lysander.Items
    2. {
    3.     public class ItemDatabaseList : ScriptableObject
    4.     {
    5.         [SerializeField]
    6.         public List<Item> itemList = new List<Item>();
    7.  
    8.         [SerializeField]
    9.         public string itemType; //assembly qualified name of the derived type of "item" the list holds
    10.         //ETC
    11.     }
    12. }
    And the database creator, which uses generics both so that I don't have to make 10 of them and so that I can cache the databases to keep more than one of each from existing:

    Code (csharp):
    1. namespace Lysander.Items
    2. {
    3.     public class CreateItemDatabase
    4.     {
    5.         [SerializeField]
    6.         public static List<ItemDatabaseList> assets = new List<ItemDatabaseList>();
    7. #if UNITY_EDITOR
    8.         public static ItemDatabaseList createItemDatabase<T>() where T : Item
    9.         {
    10.             ItemDatabaseList newList = assets.Find(t => t.itemType == typeof(T).AssemblyQualifiedName);
    11.  
    12.             if (newList != null)
    13.                 return newList;
    14.  
    15.             newList = ScriptableObject.CreateInstance<ItemDatabaseList>();
    16.             newList.itemType = typeof(T).AssemblyQualifiedName;
    17.             assets.Add(newList);
    18.  
    19.             AssetDatabase.CreateAsset(newList, string.Format("Assets/_TestCharacter/InventorySystem/Items/Databases/Resources/{0}ItemDatabase.asset", typeof(T).Name));
    20.             AssetDatabase.SaveAssets();
    21.  
    22.             return newList;
    23.         }
    24. #endif
    25.     }
    26. }
    Now, finally, the relevant portion of the editor script where I load and utilize the various databases.

    Code (csharp):
    1. namespace Lysander.Items
    2. {
    3.     public class Lys_Manager : EditorWindow
    4.     {
    5.         [MenuItem("Lysander/Inventory Manager")]
    6.         static void Init()
    7.         {
    8.             EditorWindow.GetWindow(typeof(Lys_Manager));
    9.  
    10.             AllMyBase = new List<DatabaseDetails>();
    11.  
    12.             {    //FOR RESOURCE LIST
    13.                 Object ItemDatabase = Resources.Load<ItemDatabaseList>("ResourceItemDatabase");
    14.                 if (ItemDatabase == null)
    15.                     AllMyBase.Add(new DatabaseDetails(CreateItemDatabase.createItemDatabase<Resource>()));
    16.                 else
    17.                     AllMyBase.Add(new DatabaseDetails(ItemDatabase as ItemDatabaseList));
    18.  
    19.                 AllMyBase[0].toolbarLabels = new string[] { "Create Resource", "Manage Resources" };
    20.             }
    21.  
    22.             {    //FOR CONSUMABLE LIST
    23.                 Object ItemDatabase = Resources.Load<ItemDatabaseList>("ConsumableItemDatabase");
    24.                 if (ItemDatabase == null)
    25.                     AllMyBase.Add(new DatabaseDetails(CreateItemDatabase.createItemDatabase<Consumable>()));
    26.                 else
    27.                     AllMyBase.Add(new DatabaseDetails(ItemDatabase as ItemDatabaseList));
    28.  
    29.                 AllMyBase[1].toolbarLabels = new string[] { "Create Consumable", "Manage Consumable" };
    30.             }
    31.             //
    32.             //ETC
    33.             //
    34.  
    35.             foreach (DatabaseDetails details in AllMyBase)
    36.             {
    37.                 //RESET MEMBERS
    38.             }
    39.         }
    40.  
    41.         [System.Serializable]
    42.         public class DatabaseDetails
    43.         {
    44.             [SerializeField]
    45.             public ItemDatabaseList database;
    46.             [SerializeField]
    47.             public bool showDatabase;
    48.             [SerializeField]
    49.             public int toolbarSelected;
    50.             //
    51.             //ETC
    52.             //
    53.  
    54.             public DatabaseDetails(ItemDatabaseList database)
    55.             {
    56.                 this.database = database;
    57.             }
    58.         }
    59.         //
    60.         //ETC
    61.         //
    62.     }
    63. }
    I'm sincerely hoping that my problem will be instantly (and stupidly) apparent to one of you out there. I'm not getting any errors or anything- it's just a serialization issue as far as I can tell, but I can't tell where the problem is.
     
  2. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    Fixed. Simply by changing my abstract base class "Item" into a concrete class, everything worked properly. I'm not exactly happy that I can't use an abstract class though.
     
    tfpuelma likes this.
  3. AhrenM

    AhrenM

    Joined:
    Aug 30, 2014
    Posts:
    74
    Yeah, this limitation has caused me no end of lost hair and the odd re-write.

    You can get what you want to fly, if your original item class (non abstract) derives from ScriptableObject. This introduces a bit of pain most notably that new instances need to be created via ScriptableObject.CreateInstance(); or one of it's overloads. Make sure you do you research before going down this path.

    And don't even think about creating a generic list of an Interface, unless writing your own serialization hooks sounds like fun.
     
  4. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    Yeah, it works perfectly for allowing me to create a database-each for the subtypes of "item" with only the one "CreateDatabase<T>() where T : Item".

    I've run into a few slightly annoying problems in that I had to write an exception to the CreateDatabase script in the case that T actually IS Item, rather than deriving from Item, because Item is supposed to be abstract, really. I also solved the related problem of disallowing individual "Items" to be created simply by giving it only a private parameterless constructor.

    The more annoying issue is that you can't use a variable Type when trying to access a generic function, which means I had to use the following to actually load (or create) the databases when the editor window is opened:
    Code (csharp):
    1. List<KeyValuePair<string, ItemListFunc>> databaseList = new List<KeyValuePair<string, ItemListFunc>>();
    2. databaseList.Add(new KeyValuePair<string, ItemListFunc>(typeof(Resource).Name, new ItemListFunc(CreateItemDatabase.createItemDatabase<Resource>)));
    3. databaseList.Add(new KeyValuePair<string, ItemListFunc>(typeof(Consumable).Name, new ItemListFunc(CreateItemDatabase.createItemDatabase<Consumable>)));
    4. //ETC...
    5.  
    6. for (int i = 0; i < databaseList.Count; i++)
    7. {
    8.    ItemList ItemDatabase = Resources.Load<ItemList>(databaseList[i].Key + "ItemDatabase");
    9.  
    10.    if (ItemDatabase == null)
    11.       ItemDatabase= databaseList[i].Value.Invoke() as ItemList;
    12.  
    13.    //Assign ItemDatabase to static database list
    14. }
    Where ItemListFunc is actually Func<ItemList>, so I didn't have to pull the whole System namespace into this.
     
    Last edited: Aug 10, 2015
  5. TrickyHandz

    TrickyHandz

    Joined:
    Jul 23, 2010
    Posts:
    196
    You may want to check this out: Unity Serialization In-Depth
    In that presentation, Tim Cooper explains some ways to pull off serializable lists of abstract classes. I'm not sure if it will meet your needs but it answered quite a few questions I had when dealing with a similar situation.
     
  6. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    I actually managed to get it working by making item inherit ScriptableObject and storing items in the same physical asset with the ScriptableObject list that holds them (different list instances for different item types- though they're all stored as "List<Item>", they're maintaining their true types on load now). It wasn't even that complicated, once I figured it out. I got around using the Func list above by replacing the generic type feature of my createItemDatabase function with a System.Type parameter instead, which is checked for inheriting from Item and throws an exception if it isn't, so now that part is far more streamlined as well.

    I've got it to the point where, simply by making a new Item sub-type class and and adding that type to a static list I maintain (which takes less than 10 seconds total), an entire new item database will be created for that type and it will have its own section in the editor for creating/editing/removing items from that database. Of course, any sub-type specific editor items (like changing the "weapon type" value (sword, axe, whatever) on the "weapon" item sub-type) need to be added in a conditional type-check, and as a result the "item manager" editor script is getting pretty long, now ^_^

    Thanks for the help, though.
     

    Attached Files:

    Last edited: Aug 17, 2015
    TrickyHandz likes this.
  7. Duffer123

    Duffer123

    Joined:
    May 24, 2015
    Posts:
    1,215
    @ Lysander,

    Any chance you could post more of your ultimate code here. Looks like it would prove vv helpful?
     
  8. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    I posted it in another thread that was going concurrently with this one. Let me try to find that thread really quick (also, sorry about not seeing this for a few days).

    EDIT: Found it. This hasn't been updated in a couple of months and I didn't make it with other people in mind really, but it's modular and should work fine (I think). In order to add a new item sub-type, just follow these steps:
    • Make a new script for the new sub-type in the ItemTypes directory, each sub-type needs to have its own script- you can't have many in one script or it won't work for some reason. You can just duplicate another sub-type script if you want (Quest.cs for instance) to make things easier. Be sure to change both the script name and the class name in the definition if you duplicate it, obviously.
    • Inherit from Lysander.Items.Item on the sub-type, or include the Lysander.Items namespace at the top and inherit from "Item".
    • Add this new sub-type to the ItemType enum list in "ItemEnums.cs"- you'll see the other sub-types listed there already, so it should be easy. You can name it whatever you wish.
    • Back in the new sub-type's class definition script, override "Init", then tell it to run base.Init() and change the itemType member to the new sub-type you just made in the enum script. If you just copy another item sub-type's contents, this will be perfectly clear and take 2 seconds.
    • Last, just add the new class name to the ItemTypes list in "ItemList.cs". This will add the type to "registered types" for the database system, which means a new menu will be available in the database for it. If you wish this new item sub-type to have "attributes" like weapons and armor do, then also add it to the ItemTypesWithAttributes list as well, just under that in the same file.
    • That's it- it should work.
     

    Attached Files:

    Last edited: Oct 26, 2015
  9. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    Posting again to alert you that the last response was edited, and also to share a few thoughts on this with you that would really clog-up the last post if all put in one place. Note that I'm not actually using this system in any of my active projects at the moment, I made it just to see how I could get it to work and whether it would be inferior or superior to other methods for database management that I've used over the years. For that reason, the package included is pretty rough in several ways.

    First, I highly suggest that if you plan on using it directly, rather than just as reference material, that you at the very least break the ItemManager.cs script up into parts. As it is, it runs through the list of System.Types ("ItemTypes" in the ItemList.cs file) and creates/loads databases for each of those types in exactly the same way, be they armor, weapons, consumables, etc... This worked really well when I was making this package because it meant I only needed to make a change in one place to effect the sub-menus of many different item types, but in practice it means you're going to have to make a TON of sub-type specific additions/subtractions to the UI once they start differentiating themselves from eachother a bit.

    That means that, in the ItemManager.cs file, you'll need conditional statements in the spots where you want options on the screen that are specific to the consumable sub-type, or the weapons sub-type, etc... That might work for awhile, but if the list grows longer or the sub-types more complicated in their differences, it's going to be an absolute mess. Therefore, you need to break ItemManager.cs up, for instance leaving the script to manage the menus as a whole, but having separate scripts (or at least functions) to handle the "items" in the currently loaded database differently based on their types. That way, Consumable items are displayed completely differently from weapons, and you can show and edit their unique members without a bunch of messy conditional statements.

    If you want to be super-efficient and also limit your editing too much in the future, you can break each item display into two sections- the first of which shows the members for the "item" base type (ID, name, quality, quantity, cost), and the second of which shows the members that are unique to the sub-type (weapon type, armor type, etc...). The second section can be displayed through the use of a Switch statement that runs only the function needed for that specific sub-type.

    Another matter is attributes. I don't remember exactly how far I got in the implementation of attributes on the various item types, but I do recall that I wasn't quite finished yet. Attributes are things like "strength", "defense", "attack", etc... that I was planning on using as a semi-universal system for managing modifiers and effects on usable items in the game.

    I had planned on making it so that item sub-types could have attributes selected that always existed on all of them (defense attribute for the "armor" sub-type, attack attribute for the "weapon" sub-type), while others were "optional" (fire resistance attribute). This would not be hard-coded, as attributes are "universal" and technically work on anything, but rather something enforced in the UI instead. In practice, the system would always check if the attribute exists at all rather than assuming that it does, even if it's always included on the sub-type being checked- the point in having them there by default was to limit repetition when making the database. If you have to manually add the "defense" attribute to every single piece of armor, you'll get REALLY sick of it.

    In the attribute manager, you can manage defaults as well. At the bottom you can add or remove the attributes themselves from the database, and at the top you'll be able to set the defaults that exist for each item sub-type. I know that this system was not finished- there's some aspect of it that I did not complete, but I can't recall which aspect that was. It could be that adding an attribute in the Attribute Manager, then switching over the Item Manager and adding that attribute to a piece of gear, then going back and removing the attribute from the database will not remove the attribute from the gear properly. That's just an example- I'm not sure.

    If I get some time I'll check it out maybe here in a few weeks, or if you have a specific problem just let me know. I'm really hoping that you're just looking to use it as a reference though, because it may be "close" to ready for public consumption but it's not really all of the way there (IIRC).

    I hope it helps though, either way!
     
    Last edited: Oct 26, 2015
  10. Duffer123

    Duffer123

    Joined:
    May 24, 2015
    Posts:
    1,215
    @Lysander ,

    Very very helpful. Many thanks!
     
  11. Duffer123

    Duffer123

    Joined:
    May 24, 2015
    Posts:
    1,215
    Just looking at it for reference mainly but still helpful... ;)