- Home /
Using custom property drawers with polymorphism
Hello, I can't get the CustomPropertyDrawer to work with polymorphism. Here's a very simple example code:
Models
[Serializable]
public class Animal: ScriptableObject
{
public string animalName;
}
[Serializable]
public class Monkey : Animal
{
}
[Serializable]
public class Lion : Animal
{
}
[Serializable]
[CreateAssetMenu(fileName = "myZoo", menuName = "Zoo", order = 0)]
public class Zoo : ScriptableObject
{
public string title;
public List<Animal> animals = new List<Animal>();
private void OnEnable()
{
}
public void AddLion()
{
Lion lion = ScriptableObject.CreateInstance<Lion>();
AssetDatabase.AddObjectToAsset(lion, this);
AssetDatabase.SaveAssets();
animals.Add(lion);
}
public void AddMonkey()
{
Monkey monkey = ScriptableObject.CreateInstance<Monkey>();
AssetDatabase.AddObjectToAsset(monkey, this);
AssetDatabase.SaveAssets();
animals.Add(monkey);
}
}
Zoo Editor
[CustomEditor(typeof(Zoo))]
[CanEditMultipleObjects]
public class ZooEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
if(GUILayout.Button("Add lion"))
{
(serializedObject.targetObject as Zoo).AddLion();
}
if (GUILayout.Button("Add monkey"))
{
(serializedObject.targetObject as Zoo).AddMonkey();
}
}
}
Property Drawers
[CustomPropertyDrawer(typeof(Animal))]
public class AnimalPropertyDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.LabelField(new Rect(position.x, position.y, position.width, 20), "Hello, I'm an animal");
SerializedObject serializedObject = new SerializedObject(property.objectReferenceValue as Animal);
EditorGUI.PropertyField(new Rect(position.x, position.y + 20, position.width, 20), serializedObject.FindProperty("animalName"));
}
}
[CustomPropertyDrawer(typeof(Lion))]
public class LionPropertyDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.LabelField(new Rect(position.x, position.y, position.width, 20), "Hello, I'm a lion");
SerializedObject serializedObject = new SerializedObject(property.objectReferenceValue as Lion);
EditorGUI.PropertyField(new Rect(position.x, position.y + 20, position.width, 20), serializedObject.FindProperty("animalName"));
}
}
[CustomPropertyDrawer(typeof(Monkey))]
public class MonkeyPropertyDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.LabelField(new Rect(position.x, position.y, position.width, 20), "Hello, I'm a monkey");
SerializedObject serializedObject = new SerializedObject(property.objectReferenceValue as Monkey);
EditorGUI.PropertyField(new Rect(position.x, position.y + 20, position.width, 20), serializedObject.FindProperty("animalName"));
}
}
Now, as you can see, even if the zoo has a Lion and a Monkey in the animals list, only the AnimalPropertyDrawer is used to draw both the list items. I know this is a common problem in Unity.
Is there a way to let the child classes use their own property drawer?
What's your best solution to this problem?
Answer by toxicFork · Apr 29, 2018 at 11:03 AM
The problem is that the Zoo
class has a List<Animal>
. So it seems that it will do the property drawing using "the property drawer for the Animal
type", rather than trying to look deeper into the type specified for each instance.
What you can do is implement the polymorphism handling within the AnimalPropertyDrawer
, like this:
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
[CustomPropertyDrawer(typeof(Animal))]
public class AnimalPropertyDrawer : PropertyDrawer
{
private static Dictionary<Type, PropertyDrawer> _polyPropertyDrawers;
public AnimalPropertyDrawer()
{
if (_polyPropertyDrawers == null)
{ // creating the static variable in the constructor so that it can be cached and reused
_polyPropertyDrawers = new Dictionary<Type, PropertyDrawer>();
_polyPropertyDrawers[typeof(Lion)] = new LionPropertyDrawer();
_polyPropertyDrawers[typeof(Monkey)] = new MonkeyPropertyDrawer();
// add other types here
}
}
public PropertyDrawer GetPolyPropertyDrawer(SerializedProperty property)
{
PropertyDrawer drawer;
if (!_polyPropertyDrawers.TryGetValue(property.objectReferenceValue.GetType(), out drawer))
{
return null;
}
return drawer;
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
var polyDrawer = GetPolyPropertyDrawer(property);
if (polyDrawer == null)
{
return base.GetPropertyHeight(property, label);
}
return polyDrawer.GetPropertyHeight(property, label);
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var polyDrawer = GetPolyPropertyDrawer(property);
if (polyDrawer == null)
{
EditorGUI.HelpBox(position, "Cannot find drawer for type " + property.objectReferenceValue.GetType(), MessageType.Error);
}
else
{
polyDrawer.OnGUI(position, property, label);
}
}
}
I was hoping in a way to let Unity use the specialized type but, if there's no way, this is actually a very good solution. Thank you.
Answer by Bunny83 · Apr 28, 2018 at 04:09 PM
This has nothing to do with property drawers at all. Your data classes can't be serialized by Unity's serializer since it doesn't support polymorphism for custom serializable classes. To support polymorphism your classes need to be derived either from MonoBehaviour or ScriptableObject.
PropertyDrawers do not influence what get serialized and what not. They just change the way how the serialized data is presented / visualized.
Make sure you read this page carefully
I let Animal to inherits from ScriptableObject (and edited my question acoordingly) but still I have the same behaviour.
Anyway I read the page you linked and noticed this sentence:
The downside of this is that you need to store that $$anonymous$$onobehaviour or scriptable object somewhere, and that you cannot serialize it inline efficiently.
Does that mean that every animal should be saved as an asset inside the Zoo asset?
Yes, if you derive your Animal class from ScriptableObject and want it to be serialized you have to actually store it as asset. If you don't want each animal to be a seperate asset file you would need some editor code to add the instances to your zoo asset.
" if you derive your XXX class from ScriptableObject and want it to be serialized you have to actually store it as asset." Just saw this post. Can you confirm this? I'm pretty sure I can instantiate a ScriptableObject derived class, then simply reference it from a scene object/monobehavior for it to be serialized with the scene, rather than as an asset file.
Answer by Chazmundo · Mar 15, 2021 at 11:50 PM
Hi, I know I'm a little late to the party, but inspired by this answer and my own frustration:
I've created a generic and easy to use solution for Polymorphic PropertyDrawers in Unity.
I've posted the full details here (ready to Copy & Paste into your own project): https://forum.unity.com/threads/custompropertydrawer-for-polymorphic-class.824667/#post-6702670
P.S. I am posting here as it's one of the top search results when looking for "unity polymorphic property drawer" so hopefully it'll help more people
Your answer
Follow this Question
Related Questions
An OS design issue: File types associated with their appropriate programs 1 Answer
Custom Inspector not working with inheritance. 2 Answers
How to change variables from another script? 1 Answer
Accessing subclass properties in inspector based on enum 1 Answer
How can I access an inherited method from a separate (collided) object? 1 Answer