Dynamically creating Scriptable Objects containing fields with derived type assigned

Hello everyone!
I’ve recently came across a problem that I still haven’t figured out how to deal with.

Let’s assume we have these classes:

class DialogueElement { }

class DialogueLine : DialogueElement { }

class DialogueDecision : DialogueElement { }

class DialogueData : ScriptableObject
{
    DialogueElement element;

    public DialogueElement GetElement()
    {
          return element;
    }
}

This is simplified version of what I’m working on right now.
So I create DialogueData from script during Play Mode with dynamically generated DialogueElement which can be either DialogueLine or DialogueDecision.
In result I get a single DialogueData.asset Scriptable Object.
Without exiting the Play Mode I drag and drop the generated DialogueData into a field of one of my scene object’s component which may look something like this:

class DialogueStarter : MonoBehaviour
{
    [SerializeField] DialogueData dialogueData;
  
    public void StartDialogue()
    {
         if(dialogueData.GetElement() is DialogueLine)
               Debug.Log("It's a line!");
         else if(dialogueData.GetElement() is DialogueDecision)
               Debug.Log("It's a decision!");
         else
                Debug.Log("I don't know what this is...");
    }
}

So after I put generated DialogueData on that component I invoke StartDialogue() and everything is as expected - if I generated DialogueData containing Line it says “it’s a line” and when it’s Decision it says “it’s a decision”.
But after I turn off the Play Mode and then attach the generated DialogueData to the same component it seems to lose some data about inheritance and after entering Play Mode and invoking StartDialogue() this time I get “I don’t know what this is…”.

Now, I’m looking for a soluting to this problem which doesn’t include making DialogueElement a Scriptable Object which will be also saved as a file. Ideally Dialogue Data should be just one file containing everything.

I am attaching the .unitypackage with all the setup needed to test this behaviour and see what I mean in practice. :smile:

Thank you all for reading and hopefully someone already came up with a solution! :slight_smile:

P.S. I’m using Unity 2018.3.4f1

4843832–465566–ScriptableObjectTest.unitypackage (5.66 KB)

You hit a limitation of how Unity serializes its objects. Unity doesn’t save what types objects actually are, it relies on the type of the variable to figure that out. This means, if you have a DialogueElement variable, Unity will only ever save and load a DialogueElement, even if the object is actually a subclass. This also means you can’t save variables that use an interface, since Unity won’t be able to load it without knowing the concrete type.

The exception are classes that derive from UnityEngine.Object (including ScriptableObject). Those are saved differently and include the type, supporting your use case.

In Unity 2019.3 (currently in alpha), Unity is relaxing this and introducing a [SerializedReference] attribute that will save the types but comes with other restrictions:

If you don’t want to venture into alpha territory, there’s no easy solution. You could:

  • Use a single DialogueElement type that contains all the different behaviors (merge Line and Decision into one)
  • Have a separate field on DialogueData for every possible element type
  • Make DialogueElement derive from ScriptableObject
  • Use a different serialization system (.e.g. Json.NET) that supports polymorphism
1 Like

Thank you for your answer!
Unfortunately just making DialogueElement derive from ScriptableObject still doesn’t help unless I create the files .asset for every element. Without creating that file it behaves the same as without deriving from ScriptableObject.

I really want to use ScriptableObjects instead of JSON because of simplicity of editing data in Unity Inspector.

The solution with separate fields for every type would not work as in original class it is a list of DialogueElements with order of them being crucial (I guess I still could make it work in that case but it would get extremely messy then).

The first solution with merging all classes into one should work, but again it would get very messy proporitionally to the number of “subclasses”.

So for now I still do not have a perfect solution, unless I am missing something with DialogueElement deriving from ScriptableObject.

If any new idea won’t appear in my (or someone elses on the forum) head, I will probably just go with JSON.

Yep, it’s creating the asset file which nails down the specific type of object for ScriptableObjects, so you need to do that or the type won’t stick (or rather, creating one that’s just floating around in memory instead of as an asset is contrary to the entire purpose of ScriptableObjects IMO).

If you don’t want to store them separately, you can actually place ScriptableObjects inside of other ScriptableObjects with AssetDatabase.AddObjectToAsset- it’s some extra coding and it’s really easy to corrupt a bunch of data if you aren’t careful about the way you add and remove things from the list, but it works.

I’m personally a big fan of the Json approach, and use Json.NET to simplify things with multi-stage deserializations and being able to use dynamics for simplicity. ScriptableObjects are useful for being able to put the data directly in the inspector, but eventually (as you gain an actual team and start to outsource some of the work), you’ll want to do the dialogue stuff externally. At that point, XML/Json or other readable formats are superior to a specific implementation of YAML, and easier to import through web APIs as well. Keep in mind, you can write custom inspectors for Json data files too.

ScriptableObjects also can’t be used to add additional data to your game post-release without using AssetBundles or writing your own YAML interpreter for them, which is another serious limitation.

1 Like

You can also do some scaffolding with ISerializationCallbackReceiver to convert or serialize things transparently.

For example, I made this class that allows you to have a List with different subclasses in it. It’s not very efficient but pretty easy to use and doesn’t need any external dependencies:

using System;
using System.Collections.Generic;
using UnityEngine;

namespace sttz.Core {

/// <summary>
/// Wrapper that allows to serialize a list containing derived types in Unity.
/// </summary>
/// <remarks>
/// Unity serialization doesn't handle derived types used in place of their
/// base type. I.e. a field or a list of type Base containing object of type
/// Derived will serialize its object(s) as Base.
///
/// This wrapper for List stores the types of the objects in the list as well
/// as their JSON representation so it can deserialize the list to contain
/// the same types it was saved with.
///
/// Just use <see cref="list"/> to access the underlying list and use it
/// as you would a non-wrapped list.
///
/// NOTE: JsonUtility doesn't support serializing references to other
/// Unity objects. If you need references you need to somehow serialize them
/// yourself with ISerializationCallbackReceiver.
///
/// NOTE 2: Unity doesn't support serializing generic types. You need to
/// subclass PolyList to a concrete type first:
/// `public class PolyListType : PolyList<Type&rt; { }`
/// </remarks>
[Serializable]
public class PolyList<T> : ISerializationCallbackReceiver
{
    // List containing deserialized data
    [NonSerialized] public List<T> list = new List<T>();
    // Lists containing serialized data for Unity to save
    [SerializeField] List<string> serialized;
    [SerializeField] List<string> types;

    public void OnBeforeSerialize()
    {
        if (serialized == null) {
            serialized = new List<string>();
        } else {
            serialized.Clear();
        }

        if (types == null) {
            types = new List<string>();
        } else {
            types.Clear();
        }

        foreach (var wrap in list) {
            if (wrap == null) {
                types.Add(null);
                serialized.Add(null);
            } else {
                types.Add(wrap.GetType().FullName);
                serialized.Add(JsonUtility.ToJson(wrap));
            }
        }
    }

    public void OnAfterDeserialize()
    {
        list.Clear();

        for (int i = 0; i < serialized.Count && i < types.Count; i++) {
            var typeName = types[i];
            var jsonData = serialized[i];

            var type = FindType(typeName);
            if (type == null) {
                Debug.LogWarning("PolyList: Type '" + typeName + "' not found. Falling back to base type '" + typeof(T).FullName + "'");
                type = typeof(T);
            }

            T item = (T)JsonUtility.FromJson(jsonData, type);
            list.Add(item);
        }
    }

    static Dictionary<string, Type> typeCache;

    static Type FindType(string name)
    {
        Type type;
        if (typeCache != null && typeCache.TryGetValue(name, out type)) {
            return type;
        }

        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) {
            type = assembly.GetType(name);
            if (type != null) {
                if (typeCache == null) {
                    typeCache = new Dictionary<string, Type>();
                }
                typeCache[name] = type;
                return type;
            }
        }

        return null;
    }
}

}
1 Like

That is EXACTLY what I needed! Just tried it out on simplified version that I attached in the first post and it works like a charm! Thank you very much Adrian. :slight_smile:
Also thank you, Lysander for your help. I would probably go with custom inspector for Json file if I didn’t learn about this ISerializationCallbackReceiver just now. :smile: