ScriptableObject database loses stored types on restart

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 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:

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.

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

Are the items instances of ScriptableObjects?
If not, this should shed some light on your issues: (look under General Array Serialization)

You may want to read the Serialization Best Practices Megapost. To serialize lists of non-ScriptableObject subclasses, you’ll need to implement ISerializationCallbackReceiver to handle your own serialization. Unity’s default serialization deserializes list elements as the base type.

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, 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.

2244264–149860–ItemDatabase.zip (3.33 KB)

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.

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.

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?

(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”).

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.

namespace Lysander.Items
{
    public class CreateItemDatabase
    {
        [SerializeField]
        public static List<ItemList> assets = new List<ItemList>();

#if UNITY_EDITOR
        public static ItemList createItemDatabase<T>() where T : Item
        {
            ItemList newList = assets.Find(t => t.qualifiedItemType == typeof(T).AssemblyQualifiedName);

            if (newList != null)
                return newList;

            newList = ScriptableObject.CreateInstance<ItemList>();
            newList.qualifiedItemType = typeof(T).AssemblyQualifiedName;

            assets.Add(newList);

            AssetDatabase.CreateAsset(newList, string.Format("Assets/ManagementSystems/Databases/Resources/{0}ItemDatabase.asset", typeof(T).Name));
            AssetDatabase.SaveAssets();

            return newList;
        }
#endif

    }
}

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:

class Item : ISerializationCallbackReceiver
{
    public void OnBeforeSerialize()
    {
        //Cast to weapon and see if it isn't null
        Weapon w = this as Weapon;
        if(w != null) {
             //do stuff
        }
    }

    public void OnAfterDeserialize()
    {

    }
}

Or am I totally off?

Any hints would be appreciated.

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):

public class ItemDatabase : ScriptableObject {
    public List<Item> items;
}

//=======================================
[Serializable]
public class Item : ISerializationCallbackReceiver {
    public AbstractItemInfo info; //<-- Doesn't get serialized automatically
    public string infoType; //<-- Gets serialized automatically
    public List<int> serializedItemData; //<-- Gets serialized automatically

    public void OnBeforeSerialize() {
        infoType = info.GetType().Name;
        info.SerializeData(serializedItemData);
    }

    public void OnAfterDeserialize() {
        info = Activator.CreateInstance(Type.GetType(infoType)) as AbstractItemInfo;
        info = DeserializeData(serializedItemData);
    }
}

//=======================================
public abstract class AbstractIteminfo {
    public abstract void SerializeData(List<int> data);
    public abstract void DeserializeData(List<int> data);
}

//=======================================
public class WeaponInfo : AbstractItemInfo {
    public int damage;

    public override void SerializeData(List<int> data) {
        data.Clear();
        data.Add(damage);
    }

    public override void DeserializeData(List<int> data) {
        damage = data[0];
    }
}

//=======================================
public class ArmorInfo : AbstractItemInfo {
    public int protection;

    public override void SerializeData(List<int> data) {
        data.Clear();
        data.Add(protection);
    }

    public override void DeserializeData(List<int> data) {
        protection = data[0];
    }
} // etc.

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.

Thanks. I’ll have a look tomorrow after work.
I’ll get back to you with how it goes.

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.

2245937–149992–ItemDatabaseEditor.zip (4.74 KB)

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.

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

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.

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 i do AssetDatabase.CreateAsset(item, PATH)

EDIT: Solved it even better by using AssetDatabase.AddObjectToAsset instead

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.

@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…