Changing array variable declarations for scriptable object and impact on editor

Hi,

Suppose I declare an array variable for a scriptable object, and save it. Then I change my mind about the array - maybe it is the scope or the name - so I update the script and save changes. However, this change is not implemented in the editor, the original declaration is retained.

I have fixed this problem before i.e. make Unity “forget” the original array variable and load the new one. But I have forgotten how I fixed it. Now I have an example, it is a bit more complicated than my previous situations, hope you can please help me.

Example
This is a Register class, it has a dictionary variable and I’m trying to serialise it by using lists. The problem code is these two lines specifically.
public List _keys = new List();
public List _values = new List();
The problem is that I originally declared these two lists private, hence Unity would not serialise. I declared them as public, but Unity refuses to forget their original scope.

FYI I also write a class RegisterEditor, it is used to edit the Register. I do not paste here because it’s not relevant to the issue. Flag class contains two variables: string description and int quantity.

This is an adaptation of the Adventure Game Tutorial AllConditions scriptable object and editor, I wanted to use my own nomenclature and use dictionary instead of arrays to store and retrieve stuff.

using UnityEngine;
using System.Collections.Generic;
// Game flag saves the current quantity of each condition/item.
public class Register : ResettableScriptableObject {
    public Dictionary<string, Flag> flagDict;
// This is the problem code
    public List<string> _keys = new List<string>();
    public List<Flag> _values = new List<Flag>();
    public const int levelQuantity = 0;
    public const string levelDescription = "Level";
    // Static singleton
    private static Register instance;
    private const string loadPath = "Register";
    public static Register Instance {
        get {
            if (!instance)
                instance = FindObjectOfType<Register>();
            if (!instance)
                instance = Resources.Load<Register>(loadPath);
            if (!instance)
                Debug.Log("Register has not been created yet.");
            return instance;
        }
        set { instance = value; }
    }
    public void OnBeforeSerialize() {
        //Debug.Log("on before serialise");
        _keys.Clear();
        _values.Clear();
        foreach (KeyValuePair<string, Flag> kvp in flagDict) {
            _keys.Add(kvp.Key);
            _values.Add(kvp.Value);
        }
    }
    public void OnAfterDeserialize() {
        //Debug.Log("on after serialise");
        flagDict = new Dictionary<string, Flag>();
        for (int i = 0; i < System.Math.Min(_keys.Count, _values.Count); i++)
            flagDict.Add(_keys[i], _values[i]);
    }
    // This function will be called at Start once per run of the game. Resets all condition quantities to zero.
    public override void Reset() {
        flagDict = new Dictionary<string, Flag>();
    }
    public override void AddDefaults() {
        if (!flagDict.ContainsKey(levelDescription))
            flagDict.Add(levelDescription, Flag.CreateFlag(levelDescription, levelQuantity));
    }
    // flag is satisfied only if there is exact matching quantity.
    public static bool HasMatchingFlag(Flag flag) {
        Flag savedFlag;
        Instance.flagDict.TryGetValue(flag.description, out savedFlag);
        if (savedFlag == null) return false;
        return savedFlag.quantity == flag.quantity;
    }
    // Updates the flag by a certain quantity
    public bool AddQuantity(string description, int quantity) {
        Flag savedFlag;
        Instance.flagDict.TryGetValue(description, out savedFlag);
        if (savedFlag == null) return false;
        savedFlag.quantity += quantity;
        return true;
    }
    // Updates the flag by a certain quantity
    public bool SetQuantity(string description, int quantity) {
        Flag savedFlag;
        Instance.flagDict.TryGetValue(description, out savedFlag);
        if (savedFlag == null) return false;
        savedFlag.quantity = quantity;
        return true;
    }
    // add some helper functions for the level flag
}


using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
[CustomEditor(typeof(Register))]
public class RegisterEditor : Editor {
    // These are the descriptions of the Flags. This is used for the Popups on the ConditionEditor.
    public static string[] FlagDescriptions {
        get {
            if (flagDescriptions == null) UpdateFlagDescriptions();       
            return flagDescriptions;
        }
        private set { flagDescriptions = value; }
    }
    private static string[] flagDescriptions;           
 
    private FlagEditor[] flagEditors;       
    private Register register; // This is the editor target, cast to Register type
    private string newFlagDescription = "New Flag";
    private int newFlagQuantity = 0;
    private const string creationPath = "Assets/Resources/Register.asset";                                                         
    private const float addButtonWidth = 30f;
    public string levelDescription = "Level";
    public int levelQuantity = 0;
    // Set up game flag conditions array and subeditors. Called whenever you click on Register asset.
    private void OnEnable() {
        register = (Register) target;
     
        if (Register.Instance.flagDict == null)
            Register.Instance.flagDict = new Dictionary<string, Flag>();
        if (flagEditors == null) {
            CreateEditors();
        }
    }
    // Destroy subeditors. Called when you click away from Register asset.
    private void OnDisable() {
        for (int i = 0; i < flagEditors.Length; i++) {
            DestroyImmediate(flagEditors[i]);
        }
        flagEditors = null;
    }
    // Update cache of condition descriptions.
    public static void UpdateFlagDescriptions() {
        FlagDescriptions = new string[TryGetFlagsLength()];
        int i = 0;
        foreach (string flag in Register.Instance.flagDict.Keys) {
            FlagDescriptions[i] = flag;
            i++;
        }
    }
    // Draws the editor for Register asset.
    // Lists all conditions and their editors via CreateEditors() call.
    // Draws a + button for adding a new condition.
    public override void OnInspectorGUI() {
        if (flagEditors.Length != TryGetFlagsLength ()) {
            for (int i = 0; i < flagEditors.Length; i++) {
                DestroyImmediate(flagEditors[i]);
            }
            CreateEditors ();
        }
        for (int i = 0; i < flagEditors.Length; i++) {
            flagEditors[i].OnInspectorGUI ();
        }
        if (TryGetFlagsLength () > 0) {
            EditorGUILayout.Space ();
            EditorGUILayout.Space ();
        }
        float width = EditorGUIUtility.currentViewWidth / 3f;
        EditorGUILayout.BeginHorizontal ();
        EditorGUI.indentLevel++; 
        EditorGUILayout.LabelField("Flag", GUILayout.Width(width));   
        EditorGUILayout.LabelField("Quantity", GUILayout.Width(width));
        EditorGUILayout.EndHorizontal();
        EditorGUILayout.BeginHorizontal();   
        newFlagDescription = EditorGUILayout.TextField (GUIContent.none, newFlagDescription, GUILayout.Width(width));
        newFlagQuantity = EditorGUILayout.IntField(GUIContent.none, newFlagQuantity, GUILayout.Width(width));
        if (GUILayout.Button ("+", GUILayout.Width (addButtonWidth)))
        {
            AddFlag(newFlagDescription, newFlagQuantity);
            newFlagDescription = "New Flag";
            newFlagQuantity = 0;
        }
        EditorGUI.indentLevel--;
        EditorGUILayout.EndHorizontal ();
    }
    // Creates a Condition Editor for each condition, of editor type = Game Flag
    // This condition editor is empty, and a subsequent call to ConditionEditor.OnInspectorGUI() for the RegisterEditor type will populate it with the edit fields.
    private void CreateEditors () {
        int length = TryGetFlagsLength();
        flagEditors = new FlagEditor[length];
        int i = 0;
        foreach (KeyValuePair<string, Flag> entry in Register.Instance.flagDict) {
            flagEditors[i] = CreateEditor(entry.Value) as FlagEditor;
            flagEditors[i].editorType = ConditionEditor.EditorType.Group;
            i++;
        }
    }
    // Call this function when the menu item is selected.
    [MenuItem("Assets/Create/Register")]
    private static void CreateRegisterAsset() {
        if (Register.Instance) return;
        Register newInstance = CreateInstance<Register>();
        AssetDatabase.CreateAsset(newInstance, creationPath);
        Register.Instance = newInstance;
        AddFlag(Register.levelDescription, Register.levelQuantity);
        EditorUtility.SetDirty(Inventory.Instance);
        UpdateFlagDescriptions();
    }
    // Define a condition (these definitions are shown as children of Register asset)
    // and attach a copy (with zero quantity) to Register asset.
    public static void AddFlag(string description, int quantity) {
        if (!Register.Instance) {
            Debug.LogError("Register asset has not been created yet.");
            return;
        }
        if (Register.Instance.flagDict.ContainsKey(description)) {
            Debug.Log("Register already contains flag: " + description + ". Flag not added.");
            return;
        }
        Flag newFlag = FlagEditor.CreateFlag(description, quantity);
        Undo.RecordObject(newFlag, "Created new Flag");
        AssetDatabase.AddObjectToAsset(newFlag, Register.Instance);
        AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(newFlag));
        Register.Instance.flagDict.Add(newFlag.description, newFlag);
        EditorUtility.SetDirty(Register.Instance);
        UpdateFlagDescriptions ();
    }
    // Remove condition from game flag asset and destroy it.
    public static void RemoveFlag(Flag flag) {
        if (!Register.Instance) {
            Debug.LogError("Register asset has not been created yet.");
            return;
        }
        if (flag.description == Register.levelDescription) {
            Debug.Log("Cannot remove Level flag");
            return;
        }
        Undo.RecordObject(Register.Instance, "Removing flag");
        Register.Instance.flagDict.Remove(flag.description);
        DestroyImmediate(flag, true);
        AssetDatabase.SaveAssets();
        EditorUtility.SetDirty(Register.Instance);
        UpdateFlagDescriptions ();
    }
    // Register.Instance.conditions[i]
    public static Flag TryGetFlagAt (string description) {
        Flag savedFlag;
        Register.Instance.flagDict.TryGetValue(description, out savedFlag);
        return savedFlag;
    }
    // Register.Instance.conditions.Length
    public static int TryGetFlagsLength () {
        if (Register.Instance == null || Register.Instance.flagDict == null) return 0;
        return Register.Instance.flagDict.Count;
    }
    public static int TryGetFlagDescriptionIndex(Flag flag) {
        for (int i = 0; i < FlagDescriptions.Length; i++) {
            if (FlagDescriptions[i] == flag.description) return i;
        }
        return -1;
    }
}


using UnityEngine;
public class Flag : Condition {
    public static Flag CreateFlag(string description, int quantity) {
        Flag flag = new Flag();
        flag.description = description;
        flag.quantity = quantity;
        return flag;
    }
}


using UnityEngine;
public abstract class Condition : ScriptableObject {
    public string description; // Enables player to recognise item, but internally is not used
    public int quantity;        // This is the quantity saved in GameState. Do not use this for changing quantity - that should be passed as separate parameter.
}

We will try as soon as you put your code into code tags. Help here . (You should have an Edit link at the bottom of your post so you can edit the original post)

Done! There are references to other classes, I didn’t include in case it was too much. Hope the gist is clear.

What happens if you simply just put[SerializeField]in the front of your two Lists? It usually helps for me. Although I have to admit I have never custom serialized ScriptableObjects, so it may not work in this case.

Thanks for the suggestion, it did not help, I think I should expose the list variables in my register editor, so I can visually see what is happening to them. I’ll let you know if anything interesting

Oh, I forgot to implement , ISerializationCallbackReceiver interface in the Register class. So the OnSerialise/Deserialise methods weren’t being called.

Rookie error!

1 Like

Is it working? I’m curious, because I haven’t done anything like this before so I want to learn something today. :smile:

Yep!

To summarise:
If I did not implement ISerializationCallbackReceiver interface, then it was still possible to use the Register Editor at edit time i.e. to add/remove keys to dictionary and save values. However, as soon as I ran the game, Unity would whack the dictionary and all the key-value pairs would be lost.

Therefore, this was the crux of the issue.

// Game flag saves the current quantity of each condition/item.
public class Register : ResettableScriptableObject, ISerializationCallbackReceiver {
    public Dictionary<string, Flag> flagDict;
    public List<string> _keys = new List<string>();
    public List<Flag> _values = new List<Flag>();

    public void OnBeforeSerialize() {
        //Debug.Log("on before serialise");
        _keys.Clear();
        _values.Clear();
        foreach (KeyValuePair<string, Flag> kvp in flagDict) {
            _keys.Add(kvp.Key);
            _values.Add(kvp.Value);
        }
        foreach (string k in _keys) {
            Debug.Log(k);
        }
    }
    public void OnAfterDeserialize() {
        //Debug.Log("on after serialise");
        flagDict = new Dictionary<string, Flag>();
        for (int i = 0; i < System.Math.Min(_keys.Count, _values.Count); i++)
            flagDict.Add(_keys[i], _values[i]);
        foreach (string k in _keys) {
            Debug.Log(k);
        }
    }
1 Like