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

ScriptableObject database loses stored types on restart

Discussion in 'Scripting' started by mlepp, Aug 10, 2015.

  1. mlepp

    mlepp

    Joined:
    May 22, 2013
    Posts:
    26
    Sorry for bad title... Hard to break the problem down to one row.

    I've made an editor extension for creating and managing items in my game. It's based on THIS, by BurgZergArcade.
    The scriptable object has stores a List<Item> which is populated in the editor.
    Class hierarchy:
    Item
    |
    Equipment
    ____|____
    | |
    Weapon Armor

    The database works perfectly while creating and browsing the items. I draw different controls after which type of item is in the selected slot by casting it.
    But after I restart Unity and open the item editor the database only holds a list of the base class Item with values from the default constructor.

    Why is the database "corrupted" after restart?

    See images for a better explanation.

    Before restart:

    After restart:
    [/IMG]
     
  2. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    Serialization problem most likely- if you hit Play and then Stop it'll probably wipe them as well. Can't say specifically what's having problems being serialized without looking at the code, though.
     
  3. mlepp

    mlepp

    Joined:
    May 22, 2013
    Posts:
    26
    Might be that, however I've managed to serialize all of my objects to a save file before without problem. And of they work for that should the serialization work for this as well?

    EDIT: The data is still around after Play and Stop
     
  4. ThermalFusion

    ThermalFusion

    Joined:
    May 1, 2011
    Posts:
    906
  5. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,533
  6. mlepp

    mlepp

    Joined:
    May 22, 2013
    Posts:
    26
    I've now read the Best Practices post and I think I understand why the items are being cut to only be the base class.
    His solution was to make the base class inherit from ScriptableObject. Is it possible to not to do that and still solve the issue? I don't want to create my items using CreateInstance<Item>, I like using the constructors. And since my Item class (and child classes) are just storing data, it seems a bit overkill.

    Or did I misunderstand everything?

    I attached a zip containing my simplified code. If that might help explain better.
     

    Attached Files:

  7. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,533
    Can you flatten it into one base class with no subclasses? That's the easiest solution. If not, you can add your own serialization callback.
     
  8. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    Are you adamant about keeping the items in the same database? If you make CreateDatabase generic and have it accept item-subclassed types only, you can construct individual databases for each type with not much increase in lines of code. They'll deserialize back into their proper types as well, without inheriting from ScriptableObject, even if they're all being shoved into the same Item list immediately after being loaded. That's what my current setup is, and I've never had a problem- I can give you my scripts if necessary to show you.

    EDIT: Scratch that. Looks like this topic has shown a flaw in my own code as well.
     
    Last edited: Aug 10, 2015
  9. mlepp

    mlepp

    Joined:
    May 22, 2013
    Posts:
    26
    I'd rather not flatten it out. I'll look into creating my own serialization callback, just have to do some research.

    I guess that should be possible and not much extra work.
    What is the flaw you found with your code which makes this not possible?
     
  10. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    (Something I just found, which I think I'm going to use to solve this issue on my end.)

    The generic CreateDatabase I created was cute, and it does work to make separate databases for each of the Item sub-types (quite easily), but it doesn't actually assist in deserialization in any way apparently. My flaw was that I didn't actually have any sub-class-specific fields in my version of this (I just created it as a test), so I didn't notice that they weren't deserializing into their proper subtypes. In other words, I had exactly the same problem you did, but didn't see it.

    However, I have a feeling that if you made the ScriptableObject class (the one with the Item list) generic- and a separate database for each sub-type- with a wrapper class that put all of those sub-type lists in one larger list for practical use, you'd end up with the functionality you currently have but with a more organized internal structure and no problems in serializing and deserializing the item lists (because they'd be of explicit types and not just "List<Item>").

    That said, if you're going to go that far out of your way to fix the problem, it would be better to just implement the serialization callback as TonyLi suggested.

    In case you're curious though (and keeping in mind that this won't help you with your problem), here's the CreateDatabase script I made.
    Code (csharp):
    1. namespace Lysander.Items
    2. {
    3.     public class CreateItemDatabase
    4.     {
    5.         [SerializeField]
    6.         public static List<ItemList> assets = new List<ItemList>();
    7.  
    8. #if UNITY_EDITOR
    9.         public static ItemList createItemDatabase<T>() where T : Item
    10.         {
    11.             ItemList newList = assets.Find(t => t.qualifiedItemType == typeof(T).AssemblyQualifiedName);
    12.  
    13.             if (newList != null)
    14.                 return newList;
    15.  
    16.             newList = ScriptableObject.CreateInstance<ItemList>();
    17.             newList.qualifiedItemType = typeof(T).AssemblyQualifiedName;
    18.  
    19.             assets.Add(newList);
    20.  
    21.             AssetDatabase.CreateAsset(newList, string.Format("Assets/ManagementSystems/Databases/Resources/{0}ItemDatabase.asset", typeof(T).Name));
    22.             AssetDatabase.SaveAssets();
    23.  
    24.             return newList;
    25.         }
    26. #endif
    27.  
    28.     }
    29. }
     
    Last edited: Aug 10, 2015
  11. mlepp

    mlepp

    Joined:
    May 22, 2013
    Posts:
    26
    So how would I go about implementing ISerializationCallbackReceiver?
    More exactly, what is it that I should put in the respective methods? My Item class and subclasses are all build from simple data types (string, int etc). Do I have to check if the Item is of a sub type?

    Like this:
    Code (csharp):
    1.  
    2. class Item : ISerializationCallbackReceiver
    3. {
    4.     public void OnBeforeSerialize()
    5.     {
    6.         //Cast to weapon and see if it isn't null
    7.         Weapon w = this as Weapon;
    8.         if(w != null) {
    9.              //do stuff
    10.         }
    11.     }
    12.  
    13.     public void OnAfterDeserialize()
    14.     {
    15.  
    16.     }
    17. }
    18.  
    Or am I totally off?

    Any hints would be appreciated.
     
  12. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,533
    The difficulty is that, when Unity deserializes a list, it creates all of the list elements as the base type. You can use a wrapper class. Briefly (and as pseudocode without testing):
    Code (csharp):
    1. public class ItemDatabase : ScriptableObject {
    2.     public List<Item> items;
    3. }
    4.  
    5. //=======================================
    6. [Serializable]
    7. public class Item : ISerializationCallbackReceiver {
    8.     public AbstractItemInfo info; //<-- Doesn't get serialized automatically
    9.     public string infoType; //<-- Gets serialized automatically
    10.     public List<int> serializedItemData; //<-- Gets serialized automatically
    11.  
    12.     public void OnBeforeSerialize() {
    13.         infoType = info.GetType().Name;
    14.         info.SerializeData(serializedItemData);
    15.     }
    16.  
    17.     public void OnAfterDeserialize() {
    18.         info = Activator.CreateInstance(Type.GetType(infoType)) as AbstractItemInfo;
    19.         info = DeserializeData(serializedItemData);
    20.     }
    21. }
    22.  
    23. //=======================================
    24. public abstract class AbstractIteminfo {
    25.     public abstract void SerializeData(List<int> data);
    26.     public abstract void DeserializeData(List<int> data);
    27. }
    28.  
    29. //=======================================
    30. public class WeaponInfo : AbstractItemInfo {
    31.     public int damage;
    32.  
    33.     public override void SerializeData(List<int> data) {
    34.         data.Clear();
    35.         data.Add(damage);
    36.     }
    37.  
    38.     public override void DeserializeData(List<int> data) {
    39.         damage = data[0];
    40.     }
    41. }
    42.  
    43. //=======================================
    44. public class ArmorInfo : AbstractItemInfo {
    45.     public int protection;
    46.  
    47.     public override void SerializeData(List<int> data) {
    48.         data.Clear();
    49.         data.Add(protection);
    50.     }
    51.  
    52.     public override void DeserializeData(List<int> data) {
    53.         protection = data[0];
    54.     }
    55. } // etc.
    56.  
    For brevity I just assumed all data are integers, but you could of course handle this part differently.

    A lot of inventory systems (such as in Visionpunk's UFPS, Opsive's Third Person Controller, etc.) avoid this issue by creating each item as a separate ScriptableObject.
     
  13. mlepp

    mlepp

    Joined:
    May 22, 2013
    Posts:
    26
    Thanks. I'll have a look tomorrow after work.
    I'll get back to you with how it goes.
     
  14. mlepp

    mlepp

    Joined:
    May 22, 2013
    Posts:
    26
    Ok, I've now experimented a bit and in the end I decided to go the ScriptableObject route for my items.
    HOWEVER, now I get a bunch of error when opening the editor window after Played/Stopped.
    InvalidOperationException: Operation is not valid due to the current state of the object System.Collections.Stack.Peek ()

    I don't know what I'm doing wrong.
    I could have missed something when refactoring the code.

    I attached my code if you are interested in helping.

    EDIT:
    After looking a bit it seems like the ItemDatabase (ScriptableObject) only contains the slots in the List in which it stores but there is only null in the place where the item should be.
     

    Attached Files:

    Last edited: Aug 11, 2015
  15. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,533
    If you have a custom editor, it's probably the culprit. If you google that error, there are a few suggestions on unityAnswers and stackexchange.
     
  16. Deleted User

    Deleted User

    Guest

    It would be so cool if Unity implemented FullInspector as part of the engine.
     
  17. mlepp

    mlepp

    Joined:
    May 22, 2013
    Posts:
    26
    If you see my edit. I think the error is because the List that is "returned" after serialization only contains null values.
    I just can't find why the items are serialized to null.
     
  18. mlepp

    mlepp

    Joined:
    May 22, 2013
    Posts:
    26
    I think I solved it! I didn't know that for a ScriptableObject to be placed in the list of items it must be created as an asset first.
    So before putting it in the list after CreateInstance<Item> i do AssetDatabase.CreateAsset(item, PATH)

    EDIT: Solved it even better by using AssetDatabase.AddObjectToAsset instead
     
    Last edited: Aug 11, 2015
  19. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,533
    Sounds like you got it. I'm not sure if this applies to your design, but if you save the scriptable object as an asset file you can use the same reference to it everywhere rather than duplicating effort.
     
  20. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    @mlepp would you mind sending me a copy of your fixed scripts? Simplified or not is fine. I'm trying to go the same route of using AddObjectToAsset and it does seem to work in my case (the assets themselves are nulling out after a reload). I think there must be some fundamental mistake I'm making, but after reading a hundred threads on the issue, watching an hour-long video on serialization, and giving myself a massive headache with trial-and-error, I feel I'm no closer to fixing it than I was...
     
  21. mlepp

    mlepp

    Joined:
    May 22, 2013
    Posts:
    26
    Sure, I'll do it when i get home.
    I have however got a small problem out of nowhere...
    My code worked for an hour or so, but now when I use do:
    Code (csharp):
    1.  
    2. var itemDatabase = CreateInstance<ItemDatabase>();
    3. AssetDatabse.CreateAsset(itemDatabase, "Assets/Databse/itemDB.asset");
    4.  
    Before, when it worked, the created asset had an asset icon in the project file structure and was clickable to browse the items that it contained.
    But now its got a generic grey "paper" icon and is not found when loading it, and the created items can't be browsed in the inspector anymore...
    I'm not sure why this is happening. It's almost like the ItemDatabase is not serializable anymore...
    I'll look into it when I get home.
     
  22. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    I actually just got mine working- I had all of the sub-types of "Item" in a single cs file and it was confusing the system. I thought maybe just having the base class in a separate file was enough when I started this, after I heard that cramming everything into one script was buggy, but I had the issue backwards and still ran face-first into it.

    As for your problem, are you sure you didn't get the path wrong and running into a "Directory doesn't exist" error? You're missing an "a" in your "Database".
     
  23. mlepp

    mlepp

    Joined:
    May 22, 2013
    Posts:
    26
    That is just a typo. Wrote it at work and from memory.
    I'm sure that the path is correct. I most likely just messed something up. It was late :p
     
  24. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,533
  25. mlepp

    mlepp

    Joined:
    May 22, 2013
    Posts:
    26
    I just messed up. My plan was to use
    Code (csharp):
    1. hideFlags = HideFlags.HideInHierarchy;
    on the items so that only the database was visible in the project hierarchy but accidentally put it on the database... Like I said, I messed up.

    If anyone is interested in my solution i'll attach it. I scaled away some of my more game specific fields for "clarity" (not many comments I'm afraid)
     

    Attached Files:

  26. Duffer123

    Duffer123

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

    Thanks. This could prove useful.
     
  27. TomH

    TomH

    Joined:
    Jul 10, 2012
    Posts:
    41
    As an addition to an old (but helpful) thread: Just like MonoBehavior, each SerializableObject needs to be defined in a separate file bearing the same name. Otherwise, after a restart the references to this object will be null.