Reputation: 11
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);
}
}
}
}
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
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 SerializedProperty
s 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();
}
Upvotes: 1