Search code examples
c#unity-game-enginescriptable-object

ScriptableObject resetting Array of PropertyDrawer to Length of Zero


I have a simple class called Behaviour that saves a string and a string-array. It looks like this:

[System.Serializable]
public class Behaviour {
  public string Methodname;
  public string[] Parameters;
}

As you might already expect, this class is to save a method to make it possible to plug methods into the unity inspector. Methodname is the name of the method plugged in, while Parameters are all the parameters for that method formatted in a way that a static utility class can read (these string get converted to objects to then be used as parameters. so a string of "i25" would get converted to an integer parameter of 25. the array is a string as object[] cannot be serialized and thusly not saved.

I will strip out all unnecessary logic and focus on what's going wrong. Lets just assume that we want to save a single integer into the first index of the parameters array. The PropertyDrawer of Behaviour would then look like this:

[CustomPropertyDrawer(typeof(Behaviour))]
public class BehaviourEditor : PropertyDrawer {
  private Behaviour propertyReference;

  public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
    propertyReference = fieldInfo.GetValue(property.serializedObject.targetObject) as Behaviour;
    // Check if the string array is null. This happens the very first time the scriptable object is created
    if(propertyReference.Parameters == null) propertyReference.Parameters = new string[1];
    // Check if the first index is null. In this simplified example also only happening the very first time.
    if(propertyReference.Parameters[0] == null) propertyReference.Parameters[0] = "i0";

    int value = (int)DataConverter.StringToObject(propertyReference.Parameters[index]);
    value = EditorGUILayout.IntField(value);
    propertyReference.Parameters[0] = DataConverter.ObjectToString(value);
  }
}

The DataConverter simply converts from object to string and vice versa (so int n = 9 would become "i9" or "i255" would become object x = 255). All of this logic works.

Again to clarify: This is the PropertyDrawer of Behaviour. Behaviour is a private [SerializedProperty] within a ScriptableObject.

If the array is null, the if == null triggers and puts the array Parameters to the length of one. The later logic all works, we can assign values to the int field from the EditorGUILayout and the value there gets correctly saved into the array. All of that logic works.

BUT: something is changing the Parameters-Array of Behaviour. I believe that something to be the ScriptableObject. The very next frame, Parameters is no longer null (obviously) and we try to thusly access index position 0. Which results in an index out of range exception because something changed Parameters to new string[0]. They didn't set it to null, they set it's length to 0.

Why? What could possible trigger this logic? If I make Parameters a property and set a breakpoint into the set-method, noone else calls it but my own code above, yet the array still becomes length of 0. Any ideas?


Solution

  • Unity's Serializer indeed auto-initializes any fields of a serializable type such as a list or array or string etc! -> They will never be null, worst case they will be empty.

    If you simply want defau values for your fields you can simply assign them in your class itself, no drawer needed for this:

    [Serializable]
    public class Behaviour 
    {
        public string Methodname = "ExampleMethod";
        public string[] Parameters = new string[1] { "i0" };
    }
    

    In general do not directly change values via the target in editor scripts!

    This will cause you a lot of headaches regarding marking changed objects as dirty, serialize/save values correctly persistent, handle undo/redo etc

    Always rather go through the SerializedProperty and use e.g.

    var methodName = property.FindPropertyRelative(nameof(Behavior.Methodname));
    var parameters = property.FindPropertyRelative(nameof(Behavior.Parameters));
    

    and then do e.g.

    EditorGUILayout.PropertyField(methodName);
    

    and access and assign e.g.

    parameters.arraySize = 1;
    var parameter = parameters.GetElementAtIndex(0);
    parameter.stringValue = "i0";
    

    but as said that's only an example, you really want to simply put the default values in the class, not the drawer.

    Also putting

    EditorGUI.BeginProperty(position, label, property);
    
    ...
    
    EditorGUI.EndProperty();
    

    around your property drawer content is essential for the dirty state handling! See Property Drawers