Scriptable object using custom editor loses data after reopening Unity

So I’m working on a game and I decided to create a scriptable object class to store the data for different conversations. Here’s the class:

[Serializable]
[CreateAssetMenu (fileName = "ConversationData", menuName = "ScriptableObjects/ConversationData")]
public class ConversationData : ScriptableObject
{
    public string NpcName;
    public string[] Interlocutors;
    public bool Loop;
    public List<List<Tuple<int, string>>> Conversations;
}

And since Unity doesn’t have a way (at least that I’m aware of) to show a list of lists of tuples in the inspector without making use of a custom inspector, that’s exactly what I did:

[ExecuteAlways]
[CustomEditor (typeof (ConversationData))]
public class DialogueGUI : Editor
{
    private bool _dialoguesFoldout;
    private int _dialogues, _lines, _interlocutorIndex;
    private string _text;
    private List<List<Tuple<int, string>>> _conversations;
    private List<Tuple<int, string>> _conversation;
    private ConversationData _conversationData;

    public int Dialogues 
    {
        set 
        {
            _dialogues = value < 0 ? 0 : value;
        }
    }
    public int Lines 
    { 
        set => _lines = value < 0 ? 0 : value; 
    }
    public int InterlocutorIndex 
    {
        set 
        {
            _interlocutorIndex = Mathf.Clamp (value, 0, _conversationData.Interlocutors.Length);
        }
    }


    // .
    private void OnEnable ()
    {
        _conversationData = (ConversationData) this.target;
        _dialoguesFoldout = true;
    }


    // .
    public override void OnInspectorGUI ()
    {
        base.OnInspectorGUI ();

        _dialoguesFoldout = EditorGUILayout.Foldout (_dialoguesFoldout, "Dialogues");
        if (_dialoguesFoldout == true) 
        {
            _conversations = _conversationData.Conversations != null ? _conversationData.Conversations : new List<List<Tuple<int, string>>> ();
            EditorGUI.indentLevel += 1;
            Dialogues = EditorGUILayout.IntField ("Conversations", _conversations.Count);

            if (_conversations.Count != _dialogues) 
            {
                if (_conversations.Count > _dialogues)
                {
                    while (_conversations.Count != _dialogues)
                    {
                        _conversations.RemoveAt (_conversations.Count - 1);
                    }
                }
                else 
                {
                    while (_conversations.Count != _dialogues)
                    {
                        _conversations.Add (new List<Tuple<int, string>> ()); ;
                    }
                }
            }
            for (int i = 0; i < _dialogues; i += 1) 
            {
                EditorGUILayout.LabelField ("CONVERSATION " + i);

                _conversation = _conversations <em>!= null ? _conversations *: new List<Tuple<int, string>> ();*</em>

EditorGUI.indentLevel += 1;
Lines = EditorGUILayout.IntField (“Lines”, _conversation.Count);

if (_conversation.Count != _lines)
{
if (_conversation.Count > _lines)
{
while (_conversation.Count != _lines)
{
_conversation.RemoveAt (_conversation.Count - 1);
}
}
else
{
while (_conversation.Count != _lines)
{
_conversation.Add (new Tuple<int, string> (0, “”));
}
}
}
for (int j = 0; j < _lines; j += 1)
{
EditorGUILayout.LabelField ("LINE " + j);

InterlocutorIndex = EditorGUILayout.IntField (“Interlocutor index”, _conversation[j].Item1);

EditorGUILayout.LabelField (“Text”);

_text = EditorGUILayout.TextArea (_conversation[j].Item2, new GUILayoutOption[] { GUILayout.Height (45) });
conversations*[j] = new Tuple<int, string> (interlocutorIndex, text);*
}

EditorGUI.indentLevel -= 1;
}_

_conversationData.Conversations = _conversations;

if (GUI.changed == true)
{
EditorUtility.SetDirty (_conversationData);
EditorSceneManager.MarkSceneDirty (SceneManager.GetActiveScene ());
}
this.serializedObject.ApplyModifiedProperties ();
}
}
}
This seems to work fine when it comes to edit and store the different values of the list I’ve previously mentioned. However, I quickly found out that the data of that list dissapeared when closing and reopening the project for some reason (yes, only the info contained on the list, not the rest of variables that make up the class). After looking for some info on different forums, I learned of the existence of functions like “SetDirty” or “ApplyModifiedProperties” (which, as you can see, I’m using inside the “OnInspectorGUI” method after modifying the scriptable object) that are supposedly meant to fix these issues, but I haven’t been able to solve the problem this way. I don’t know if I’m using them wrong, if there’s some stupid detail I’m missing that’s provoking all of this or it’s some kind of Unity bug. Whathever this might be, I’d appreciate the help a lot, I’ve been stuck on this for a few days now and it’s becoming kind of frustrating…
Oh, by the way, the version of Unity that I’m using is 2020.1.15f1.

It’s great to see you did some research before posting here. However you have missed one important detail. Custom editors can not change how and what is serialized, only how the serialized data is presented. That means if something isn’t serialized before you create your custom editor, it won’t be serialized with the custom editor either.

Specifically in your case Unity’s serialization system does not support your “Conversations” list.

public List<List<Tuple<int, string>>> Conversations;

I don’t have a Unity installation at hand, but as far as I can remember the generic Tuple class was never supported to begin with. Further more jagged arrays, multidimensional arrays and lists of lists are not supported either. For more information see the script serialization documentation.

You can have nested arrays, but only with a custom serializable class at each stage. So the array / list element type is a serializable class which in turn can contain another List of a different class. Be warned that Unity has a serialization depth limit. In the past it was 7 layers but I read somewhere recently that it has been increased slightly.

While the generic Tuple class is certainly quite handy, I would recommend to use your own custom type as you can give the fields proper and meaningful names. So you could do something like this:

public class ConversationData : ScriptableObject
{
    public string NpcName;
    public string[] Interlocutors;
    public bool Loop;
    public List<Conversation> Conversations;
}

[System.Serializable]
public class Conversation
{
    public List<ConversationLine> lines;
}

[System.Serializable]
public class ConversationLine
{
    public int interlocutorIndex;
    public string text;
}

This construct can be serialized by Unity. You don’t need any custom inspector for your scriptableobject. However if you want to customize the editor, I would recommend to look into PropertyDrawers. Usually you only need some parts to look differently. For example a PropertyDrawer for your ConversationLine class could display the text and int field inline which would make the editing a bit simpler.