How to use a custom property drawer for class used in array.

I have a serializable class called Event Option with a custom Property Drawer. It appears correctly in the inspector for fields on objects of type EventOption. But if I wanted to have a field which is an array of EventOptions I find that editing one in the array, edits all of them.

So I have the following serializable class.

    [Serializable]
    public class EventOption
    {
        [SerializeField]
        private string text;
        [SerializeField]
        [Min(0)]
        private int value;
        [SerializeField]
        private bool hasTask;
        [SerializeField]
        private GameTask task;

        internal string Text => text;
        internal int PopHappiness => value;
        internal bool HasTask => hasTask;
        internal GameTask Task => task;
    }

With the following Custom Property Drawer.

[CustomPropertyDrawer(typeof(EventOption))]
    public class EventOptionDrawer : PropertyDrawer
    {
        private Rect _lastRect;

        private float _totalHeight;
        private float _labelHeight;
        private SerializedProperty _text;
        private float _textHeight;
        private SerializedProperty _value;
        private float _valueHeight;
        private SerializedProperty _hasTask;
        private float _hasTaskHeight;
        private SerializedProperty _task;
        private float _taskHeight;

        public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
        {
            _totalHeight = 0;

            _labelHeight = EditorGUIUtility.singleLineHeight;
            _totalHeight += _labelHeight;

            _text = property.FindPropertyRelative("text");
            _textHeight = EditorGUI.GetPropertyHeight(_text);
            _totalHeight += _textHeight;

            _value = property.FindPropertyRelative("value");
            _valueHeight = EditorGUI.GetPropertyHeight(_value);
            _totalHeight += _valueHeight;

            _hasTask = property.FindPropertyRelative("hasTask");
            _hasTaskHeight = EditorGUI.GetPropertyHeight(_hasTask);
            _totalHeight += _hasTaskHeight;

            _task = property.FindPropertyRelative("task");
            if (_hasTask.boolValue)
            {
                _taskHeight = EditorGUI.GetPropertyHeight(_task, true);
                _totalHeight += _taskHeight;
            }

            return _totalHeight;
        }
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            EditorGUI.BeginProperty(position, label, property);

            Rect labelRect = FirstRect(position, _labelHeight);
            EditorGUI.PrefixLabel(labelRect, label);

            EditorGUI.indentLevel++;
            GenerateProperty(_text, _textHeight);

            GenerateProperty(_value, _valueHeight);

            GenerateProperty(_hasTask, _hasTaskHeight);

            if (_hasTask.boolValue)
            {
                EditorGUI.indentLevel++;
                GenerateProperty(_task, _taskHeight);
                EditorGUI.indentLevel--;
            }
            EditorGUI.indentLevel--;

            EditorGUI.EndProperty();
        }

        private void GenerateProperty(SerializedProperty property, float propertyHeight)
        {
            Rect rect = NextRect(propertyHeight);
            EditorGUI.PropertyField(rect, property, true);
        }

        private Rect FirstRect(Rect position, float propertHeight)
        {
            _lastRect = new(position.x, position.y, position.width, propertHeight); ;
            return _lastRect;
        }

        private Rect NextRect(float propertyHeight)
        {
            _lastRect = new(_lastRect.x, _lastRect.y+_lastRect.height, _lastRect.width, propertyHeight);
            return _lastRect;
        }
    }

Which is used in the following scriptable object.

    public class GameEvent : ScriptableObject
    {
        [SerializeField]
        private string id;
        [SerializeField]
        private string title;
        [SerializeField]
        private Sprite image;
        [SerializeField]
        [TextArea(3, 8)]
        private string description;
        [SerializeField]
        private EventOption testOption;
        [SerializeField]
        private EventOption[] eventOptions;

Now testOption appears correctly. But editing any of the eventOptions in the array will edit all of them.
8784736--1193047--serilizable class in array.gif

See my post here:

This is probably because you’re storing the SerializedPropertys (the references to them) in GetPropertyHeight, and reading from those in OnGUI.

My guess on codeflow:
When rendering a List, the Editor will call GetPropertyHeight() on ALL objects in the list, so it can calculate the height of the list as a whole. You’re overwriting the SerializedProperty-instances with the ones that reference the very last instance in the list.

Oh I see. I will try rework it and let you know.

Okay, my new code works in both cases. Thanks.

    [CustomPropertyDrawer(typeof(EventOption))]
    public class EventOptionDrawer : PropertyDrawer
    {
        private Rect _lastRect;

        public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
        {
            var totalHeight = EditorGUIUtility.singleLineHeight;

            totalHeight += EditorGUI.GetPropertyHeight(property.FindPropertyRelative("text"));

            totalHeight += EditorGUI.GetPropertyHeight(property.FindPropertyRelative("value"));

            var hasTask = property.FindPropertyRelative("hasTask");
            totalHeight += EditorGUI.GetPropertyHeight(hasTask);

            if (hasTask.boolValue)
            {
                totalHeight += EditorGUI.GetPropertyHeight(property.FindPropertyRelative("task"));
            }

            return totalHeight;
        }
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            EditorGUI.BeginProperty(position, label, property);

            Rect labelRect = FirstRect(position, EditorGUIUtility.singleLineHeight);
            EditorGUI.PrefixLabel(labelRect, label);

            EditorGUI.indentLevel++;
            GenerateProperty(property.FindPropertyRelative("text"));

            GenerateProperty(property.FindPropertyRelative("value"));

            var hasTask = property.FindPropertyRelative("hasTask");
            GenerateProperty(hasTask);

            if (hasTask.boolValue)
            {
                EditorGUI.indentLevel++;
                GenerateProperty(property.FindPropertyRelative("task"));
                EditorGUI.indentLevel--;
            }
            EditorGUI.indentLevel--;

            EditorGUI.EndProperty();
        }

        private void GenerateProperty(SerializedProperty property)
        {
            Rect rect = NextRect(EditorGUI.GetPropertyHeight(property,true));
            EditorGUI.PropertyField(rect, property, true);
        }

        private Rect FirstRect(Rect position, float propertHeight)
        {
            _lastRect = new(position.x, position.y, position.width, propertHeight); ;
            return _lastRect;
        }

        private Rect NextRect(float propertyHeight)
        {
            _lastRect = new(_lastRect.x, _lastRect.y+_lastRect.height, _lastRect.width, propertyHeight);
            return _lastRect;
        }
    }

One final hint: You should reset _lastRect to the value of labelRect in your OnGUI (around line 30);
_lastRect = labelRect;
That will ensure that you don’t have any ‘previous data’ in there.

That’s what FirstRect should be doing I think.

Missed that one. It does indeed