CustomPropertyDrawer on inherited ScriptableObjects

Hello all,

I’ve been struggling today with this issue and hopefully someone here can help me.

So, I have a class derived from a ScriptableObject.

[System.Serializable]
public class CharacterSlot : ScriptableObject
{
    public void OnEnable()
    {
        hideFlags = HideFlags.DontSave;
    }
}

And after that I have another class derived from CharacterSlot

[System.Serializable]
public class InheritedSlot : CharacterSlot
{
}

In another scriptable object (which I actually save as an asset) I have a list of character slots:

public class CharacterSystem : ScriptableObject
{
    public CharacterSystem parent = null;
    public List<CharacterSlot> slots = new List<CharacterSlot>();
}

Now, I have custom property drawers for all these classes:

[CustomPropertyDrawer(typeof(CharacterSlot), false)]
public class CharacterSlotDrawer : PropertyDrawer

[CustomPropertyDrawer(typeof(InheritedSlot))]
public class InheritedSlotDrawer : PropertyDrawer

[CustomPropertyDrawer(typeof(CharacterSystem))]
public class CharacterSystemDrawer : PropertyDrawer

The issue is that when iterating over the slots and calling PropertyField, it won’t call the property drawer for the InheritedSlot. In fact for some reason it won’t even be compiled in since visual studio keeps saying that the breakpoints will never reach. Instead, it will always use custom property drawer for the CharacterSlot class (even though I specifically set the useForChildren flag to false). If I remove this property drawer and only keep the one for the InheritedSlot it will draw a list with every element set to “Type mismatch”.

I naturally did a bit of digging and I notice that all the properties have the type set to “PPtr<$CharacterSlot>” which might be fine given the fact that this type is given by the list property (I’m guessing here). The objectReferenceValue does contain a value of the InheritedSlot and yet the PropertyDrawer is the one for the CharacterSlot.

The techniques I used there are from here

Note that everything serializes well so that’s not an issue in this case (ScriptableObjects are used to get around the “Serializing polymorphic objects” issue)

Does anyone know what the proper usage should be here or if this a bug?

Regards,
Lorin

I’m unable to run my own test, but I would suspect this is an artifact of the drawing of the list. Does the same behavior occur when you have the slots as fields directly on your object? in other words, instead of trying to draw a list of InheritedSlots inside of a list of BaseSlots, just draw a InheritedSlot inside a BaseSlot.

// instead of
public List<Base> slots;
// test
public Base slot;

If a single direct field draws properly, then I suspect this is a unity bug.

I just tested with a single base field and the behaviour is identic. It uses the property drawer for the base and if that one does not exist, it will draw as a ObjectProperty with the type mismatch on it.

I’m starting to think this is the expected behaviour but I’m still clueless as to how to get over it. The only option I see so far is to have just a base drawer, check the type of the objectReferenceValue and adapt my logic to that but it is such a big hack, not to mention that all my extensibility here goes down the drain ( I plan on creating a asset store pack with this).

I’m not the most experienced with editor scripts, but could you have your InheritedSlotDrawer inherit from CharacterSlotDrawer, and then extend/override the drawing? I’m not sure how the binding works between class and attribute, but maybe that can work?

I also tried that and I’m affraid it doesn’t work :frowning:

Basically, the Inspector looks at your class’s metadata and determines which drawer to use before it sees the data the class actually contains. The Slots in your CharacterSystem are of Type CharacterSlot so the inspector will always “pre-load” the drawer assigned for CharacterSlot, even if the slot does have an inherited type, it grabs the drawer for CharacterSlot. This is default Inspector behavior.

If you want to draw your slots more abstractly you need to manually find each slots concrete type, explicitly convert it to its type, and then fetch its drawer. I wrote some Extension Methods a while back that lets me do just that.

 /// <summary>
        /// takes an Abstract property and finds it's Concrete class to calculate the property's needed height
        /// </summary>
        /// <returns>The height for the concrete class</returns>
        public static float AbstractHeight(this SerializedProperty property)
        {
            if (property == null) throw new ArgumentNullException (NullProperty);
            SerializedProperty p_iterator;
            bool useEndProperty = true;

            if (property.propertyType != SerializedPropertyType.ObjectReference)
            {
                p_iterator = property.Copy();
            }
            else
            {
                if (property.objectReferenceValue == null)
                    return EditorGUIUtility.singleLineHeight;
                Type concreteType = property.objectReferenceValue.GetType();
                UnityEngine.Object wrapped = property.objectReferenceValue;
                wrapped = (UnityEngine.Object) Convert.ChangeType(wrapped,concreteType);
               
                SerializedObject so_wrapped = new SerializedObject(wrapped);
                if (so_wrapped == null) throw new ArgumentNullException (NullSO);
               
                p_iterator = so_wrapped.GetIterator();
                useEndProperty = false;
            }

            float height = 0;
            bool  enterchildren = true;

            var   p_end = p_iterator.Copy();
            if(string.IsNullOrEmpty(p_iterator.propertyPath))
            {
                p_iterator.Next(true);
                if(useEndProperty) p_end.Next(true);
                //navigate to 1st valid property
            }
            if(useEndProperty)
                p_end = p_end.GetEndProperty(false);
           
            while (p_iterator.NextVisible(enterchildren))
            {
                if(p_iterator.propertyPath == "m_Script") continue;
                if(useEndProperty && p_iterator.propertyPath == p_end.propertyPath) break;

                height += EditorGUI.GetPropertyHeight(p_iterator);
                enterchildren = false;
            }
            return height;
        }

        /// <summary>
        /// Allows you to take an object represented as an abstract type and draws all its child properties as if it was the concrete type
        /// </summary>
        /// <returns>Returns true if the Property was modified, false otherwise.</returns>
        public static bool AbstractField(this SerializedProperty property,Rect rect, bool autoSave = true)
        {
            if (property == null) throw new ArgumentNullException (NullProperty);
           
            bool hasChanged =false;
            bool enterchildren = true;
            SerializedProperty p_iterator = null;
            Rect r_prop = new Rect(rect.xMin,rect.yMin,rect.width,0);
            switch(property.propertyType)
            {
                case SerializedPropertyType.ObjectReference:
                    if (property.objectReferenceValue == null)
                    {
                        //field is null show the object field instead so they can inject an instance
                        EditorGUI.BeginChangeCheck ();
                        EditorGUI.PropertyField (rect,property,GUIContent.none,true);
                        hasChanged = EditorGUI.EndChangeCheck();
                        if(hasChanged && autoSave)
                            property.SaveModifiedProperties();
                        return hasChanged;
                    }
                   
                    Type concreteType = property.objectReferenceValue.GetType();
                    UnityEngine.Object wrapped = property.objectReferenceValue;
                    wrapped = (UnityEngine.Object) Convert.ChangeType(wrapped,concreteType);
                   
                    SerializedObject so_wrapped = new SerializedObject(wrapped);
                    if (so_wrapped == null) throw new ArgumentNullException (NullSO);
                   
                    p_iterator = so_wrapped.GetIterator();
                    EditorGUI.BeginChangeCheck ();
                    while (p_iterator.NextVisible(enterchildren))
                    {
                        if(p_iterator.propertyPath == "m_Script") continue;
                        r_prop.y += r_prop.height ;
                        r_prop.height = EditorGUI.GetPropertyHeight(p_iterator);
                        EditorGUI.PropertyField(r_prop,p_iterator,true);
                       
                        enterchildren = false;
                    }
                    hasChanged = EditorGUI.EndChangeCheck();
                    if(hasChanged && autoSave)
                        so_wrapped.ApplyModifiedProperties();
                    return hasChanged;

                case SerializedPropertyType.Generic:
                    p_iterator = property.Copy();
                    var p_end = p_iterator.GetEndProperty(false);
                    EditorGUI.BeginChangeCheck ();
                    while (p_iterator.NextVisible(enterchildren))
                    {
                        if(p_iterator.propertyPath == p_end.propertyPath) break;
                        if(p_iterator.propertyPath == "m_Script") continue;
                        var content = new GUIContent(p_iterator.displayName);
                        r_prop.y += r_prop.height ;
                        r_prop.height = EditorGUI.GetPropertyHeight(p_iterator);
                        EditorGUI.PropertyField(r_prop,p_iterator,content,true);
                        enterchildren = false;
                    }
                    hasChanged = EditorGUI.EndChangeCheck();
                    if(hasChanged && autoSave)
                        property.SaveModifiedProperties();
                    return hasChanged;
                default:
                    EditorGUI.BeginChangeCheck ();
                    EditorGUI.PropertyField (rect,property,GUIContent.none,true);
                    hasChanged = EditorGUI.EndChangeCheck();
                    if(hasChanged && autoSave)
                        property.SaveModifiedProperties();
                    return hasChanged;
            }
        }
1 Like

Hm, the way I see it, you manually draw the properties of a ScriptableObject. The problem is that I have a custom drawer for the scriptable object itself and I can’t seem to invoke the drawer.

Does anyone have an idea how to invoke the drawer?

If you want to draw the content of the ScriptableObject instead of just its object field reference:

var editor = Editor.CreateEditor(yourScriptableObjectInstance);
editor.OnInspectorGUI();

The code above is just an example. Ideally you’ll create the editor once in OnEnable, not every time you draw the main inspector GUI.

1 Like