It's a Doge
It's a Doge

Reputation: 11

Unity custom editor losing values on Playmode

I am working on my personal toy project and custom editor is causing me some trouble.

I realized that all variables must be serializable to store values even after the Unity client is terminated.

Sofar it works fine with enum, int and array but for some reason two dimentional array is keep losing it's variables. I wonder why this happens on multi dimentional arrays.

here's a code that I am trying to understand.

StaticBlockData.cs (ScriptableObject script)

using UnityEngine;


namespace Scriptable
{
    [System.Serializable]
    [CreateAssetMenu(fileName = "Block", menuName = "Scriptable/Data")]
    public class StaticBlockData : ScriptableObject
    {
        public BlockValue e_blockValue = 0;
        public int i_blockCount = 0;
        public bool[] b_slots = new bool[9];
        public bool[,] b_slots2 = new bool[3,3];
    }
}

BlockShapeCustomEditor.cs (Custom Editor script)

using Scriptable;
using UnityEditor;
using UnityEngine;

namespace FACustomEditor
{
    // Custom Editor targets "StaticBlockData" script
    [CustomEditor(typeof(StaticBlockData)), CanEditMultipleObjects]
    // Inheritant UnityEditor.Editor script
    public class BlockShapeCustomEditor : Editor
    {
        /// <summary>
        /// Drawing GUI on inspector mode
        /// </summary>
        public override void OnInspectorGUI()
        {
            // Define current target script data
            StaticBlockData blockData = (StaticBlockData)target;

            // Create enum popup inspector GUI
            blockData.e_blockValue = (BlockValue)EditorGUILayout.EnumPopup("Block Value", blockData.e_blockValue);
            // Create int slider inspector GUI
            blockData.i_blockCount = EditorGUILayout.IntSlider("Block Count", blockData.i_blockCount, 1, 9);


            // Create a label for the boolean field
            EditorGUILayout.LabelField("Block Shape");

            // Start a horizontal layout group
            EditorGUILayout.BeginHorizontal();

            // Create a boolean field
            for (int y = 0; y < 3; y++)
            {
                for (int x = 0; x < 3; x++)
                {
                    // Create toggle inspector GUI with maximum width size (By setting maximum width size GUI can trim the space between each elements)
                    blockData.b_slots[x + (y * 3)] = EditorGUILayout.Toggle(blockData.b_slots[x + (y * 3)], GUILayout.MaxWidth(20));
                }

                // Change line after every 3 toggle creation
                EditorGUILayout.EndHorizontal();
                EditorGUILayout.BeginVertical();
                EditorGUILayout.EndVertical();
                EditorGUILayout.BeginHorizontal();
            }

            EditorGUILayout.EndHorizontal();



            // Create a label for the boolean field
            EditorGUILayout.LabelField("Block Shape2");

            // Start a horizontal layout group
            EditorGUILayout.BeginHorizontal();

            // Create a boolean field
            for (int y = 0; y < 3; y++)
            {
                for (int x = 0; x < 3; x++)
                {
                    // Create toggle inspector GUI with maximum width size (By setting maximum width size GUI can trim the space between each elements)
                    blockData.b_slots2[x, y] = EditorGUILayout.Toggle(blockData.b_slots2[x, y], GUILayout.MaxWidth(20));
                }

                // Change line after every 3 toggle creation
                EditorGUILayout.EndHorizontal();
                EditorGUILayout.BeginVertical();
                EditorGUILayout.EndVertical();
                EditorGUILayout.BeginHorizontal();
            }

            EditorGUILayout.EndHorizontal();


            // Send true signal when changes happen on GUI
            if (GUI.changed)
            {
                // Dirty save when every the elements in "StaticBlockData" has changed
                // Save the change progress in driver
                EditorUtility.SetDirty(blockData);
            }
        }
    }
}

enter image description here

I've tried change it to private and serialized it. I've tried to create integer two dimentional array. Same thing happen.

Upvotes: 0

Views: 418

Answers (1)

derHugo
derHugo

Reputation: 90813

  • I wonder why this happens on multi dimentional arrays.

    Because the

     public bool[,] b_slots2 = new bool[3,3];
    

    is not serializable by Unity (see Script Serialization Rules) so it can never be saved anyway!

    Note: Unity doesn’t support serialization of multilevel types (multidimensional arrays, jagged arrays, dictionaries, and nested container types). If you want to serialize these, you have two options: ...

    In your case you don't seem to really need those two options at all though. Simply make sure that you implement an according getter/setter method that handles your index conversion.

    This is not only affecting you when entering playmode! Even worse: All your settings would be lost when closing and opening Unity or in a build ;)

  • In general: You go directly through the target which doesn't do any dirty state handling and therefor no saving, and no Undo/Redo either! You do this later on "manually" but I would always rather go through the serializedObject and SerializedPropertys which handle all the mentioned automatically!

  • A minor one: For the slider you can directly use the [Range] attribute, no need for a custom editor code here.

So I would probably rather have e.g.

[CreateAssetMenu(fileName = "Block", menuName = "Scriptable/Data")]
public class StaticBlockData : ScriptableObject
{
    public BlockValue e_blockValue = 0;
    [Range(1, 9)] public int i_blockCount = 1;
    [SerializeField] private bool[] b_slots = new bool[9];

    public bool GetSlot(int x, int y)
    {
        return b_slots[GetFlatIndex(x, y)];
    }

    // if needed at all
    public bool SetSlot(int x, int y, bool value)
    {
        b_slots[GetFlatIndex(x, y)] = value;
    }

    public static int GetFlatIndex(int x, int y)
    {
        return x + (y * 3);
    }
}

and then

namespace FACustomEditor
{
    [CustomEditor(typeof(StaticBlockData)), CanEditMultipleObjects]
    public class BlockShapeCustomEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            // little bonus for displaying the default script field ;)
            using (new EditorGUI.DisabledScope(true))
            {
                EditorGUILayout.ObjectField("Script", MonoScript.FromScriptableObject((StaticBlockData)target), typeof(StaticBlockData), false);
            }
        
            EditorGUILayout.Space();

            // loads current serialized values
            serializedObject.Update();

            // use the default drawers for the first two fields
            EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(StaticBlockData.e_blockValue)));
            EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(StaticBlockData.i_blockCount)));

            // So until here you actually didn't need any custom inspector

            // get the list property
            var b_slots = serializedObject.FindProperty("b_slots");

            EditorGUILayout.LabelField("Block Shape");

            // Create a boolean field
            for (var y = 0; y < 3; y++)
            {
                // you had a couple of nested horizontal and vertical scopes
                // actually all you need is a horizontal scope for each row (y)
                using (new EditorGUILayout.HorizontalScope())
                {
                    for (var x = 0; x < 3; x++)
                    {
                        var element = b_slots.GetArrayElementAtIndex(StaticBlockData.GetFlatIndex(x, y));
                        EditorGUILayout.PropertyField(element, GUIContent.none, GUILayout.MaxWidth(20));
                    }
                }
            }

            // writes back any changes and handles dirty marking, undo/redo and saving
            serializedObject.ApplyModifiedProperties();
        }
    }
}

And assuming the i_blockCount is actually just the amount of enabled slots you could as well let your inspector process it as well and display it as read-only

public override void OnInspectorGUI()
{
    using (new EditorGUI.DisabledScope(true))
    {
        EditorGUILayout.ObjectField("Script", MonoScript.FromScriptableObject((StaticBlockData)target), typeof(StaticBlockData), false);
    }

    EditorGUILayout.Space();

    serializedObject.Update();

    EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(StaticBlockData.e_blockValue)));

    var blockCount = serializedObject.FindProperty(nameof(StaticBlockData.i_blockCount));

    // draw as read-only -> User can not change manually
    using (new EditorGUI.DisabledScope(true))
    {
        EditorGUILayout.PropertyField(blockCount);
    }

    var b_slots = serializedObject.FindProperty("b_slots");

    EditorGUILayout.LabelField("Block Shape");

    // initially reset to 0
    blockCount.intValue = 0;

    for (var y = 0; y < 3; y++)
    {
        using (new EditorGUILayout.HorizontalScope())
        {
            for (var x = 0; x < 3; x++)
            {
                var element = b_slots.GetArrayElementAtIndex(StaticBlockData.GetFlatIndex(x, y));
                EditorGUILayout.PropertyField(element, GUIContent.none, GUILayout.MaxWidth(20));

                // increase for each ticked slot
                if (element.boolValue)
                {
                    blockCount.intValue++;
                }
            }
        }
    }

    serializedObject.ApplyModifiedProperties();
}

enter image description here

Upvotes: 1

Related Questions