I’d like to make an inspector for a state machine behavior with the capability of adding multiple scriptableObjects that inherit from the same base class.
I have PropertyDrawers written for each of the child Classes, so I believed that I could get them to draw using PropertyField(). However, because I keep them in a collection of the base class type, What gets drawn is an object reference field instead of the property drawer.
Below is the skeleton of my code- just the important stuff. Starting with the non-Editor code:
public class ExampleBehaviour : StateMachineBehaviour
{
//Collection of custom classes in base type
public BaseClass[] MyClasses;
}
public class BaseClass : ScriptableObject
{
public virtual void someMethod()
{
//empty method to override
return;
}
}
public class ChildClassA : BaseClass
{
public override void someMethod()
{
//implementation A
base.someMethod();
}
}
public class ChildClassB : BaseClass
{
public override void someMethod()
{
//implementation B
base.someMethod();
}
}
And below is the Editor Code
[CustomPropertyDrawer(typeof(ChildClassA))]
public class ChildClassA_Drawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
//GUI
}
}
[CustomPropertyDrawer(typeof(ChildClassB))]
public class ChildClassB_Drawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
//GUI
}
}
[CustomEditor(typeof(ExampleBehaviour))]
public class ExampleBehaviorInspector : Editor
{
ExampleBehaviour example;
List<BaseClass> inspectorCollection = new List<BaseClass>();
void OnEnable()
{
example = (ExampleBehaviour)target;
inspectorCollection = example.MyClasses.ToList();
}
public override void OnInspectorGUI()
{
serializedObject.Update();
int_selector = EditorGUILayout.Popup(int_selector, ChildClassTypes); //Gui to pick which type to add
if (GUILayout.Button("ADD NEW"))
{
switch (int_selector)
{
case 0:
{
inspectorCollection.Add(ScriptableObject.CreateInstance<ChildClassA>());
break;
}
case 1:
{
inspectorCollection.Add(ScriptableObject.CreateInstance<ChildClassB>());
break;
}
}
example.MyClasses = inspectorCollection.ToArray(); //update the target
}
for (int i = 0; i < example.MyClasses.Length; i++) //draw all the classes in the target.
{
SerializedProperty MyClassesProperty = serializedObject.FindProperty("MyClasses");
SerializedProperty ElementProperty = MyClassesProperty.GetArrayElementAtIndex(i);
EditorGUILayout.PropertyField(ElementProperty);
}
serializedObject.ApplyModifiedProperties();
}
}
How can I get the correct property drawer to draw for the elements in the array?
I don’t have a lot of experience with property drawers but I think this should work. I really didn’t test it though since I didn’t feel like writing up all the classes and Custom Prop drawers to see. Should be an easy thing to pop in and test though:
[CustomEditor(typeof(ExampleBehaviour))]
public class ExampleBehaviorInspector : Editor
{
ExampleBehaviour example;
List<BaseClass> inspectorCollection = new List<BaseClass>();
List<int> selections = new List<int>();
void OnEnable()
{
example = (ExampleBehaviour)target;
inspectorCollection = example.MyClasses.ToList();
}
public override void OnInspectorGUI()
{
serializedObject.Update();
int_selector = EditorGUILayout.Popup(int_selector, ChildClassTypes); //Gui to pick which type to add
if (GUILayout.Button("ADD NEW"))
{
switch (int_selector)
{
case 0:
{
inspectorCollection.Add(ScriptableObject.CreateInstance<ChildClassA>());
selections.Add(0);
break;
}
case 1:
{
inspectorCollection.Add(ScriptableObject.CreateInstance<ChildClassB>());
selections.Add(1);
break;
}
}
example.MyClasses = inspectorCollection.ToArray(); //update the target
}
for (int i = 0; i < example.MyClasses.Length; i++) //draw all the classes in the target.
{
//SerializedProperty MyClassesProperty = serializedObject.FindProperty("MyClasses");
//SerializedProperty ElementProperty = MyClassesProperty.GetArrayElementAtIndex(i);
//EditorGUILayout.PropertyField(ElementProperty);
switch (selections[i])
{
case 0:
ChildClassA myObj = inspectorCollection[i] as ChildClassA;
SerializedObj serialObj = new SerializedObj(myObj);
EditorGUILayout.ObjectField(serialObj);
break;
case 1:
ChildClassB myObj = inspectorCollection[i] as ChildClassB;
SerializedObj serialObj = new SerializedObj(myObj);
EditorGUILayout.ObjectField(serialObj);
break;
}
}
serializedObject.ApplyModifiedProperties();
}
}
I’m not 100% sure the Property drawer will kick in if its being laid out as a SerializeObj. Though i’m not sure how to turn a SerializedObject into a Serialized Property.
also just a quick note. the code I posted previously is for drawing the classes polymorphically.
if all of your classes were the same concrete type and you wanted to show all their properties, PropertyField would work just fine, you’d just need to set the includeChildren parameter to true in the call (defaults to false, thus showing the ObjectField)
I wondered if you could go into some detail regarding the extension- I noticed that this allows the default inspector to draw, but doesn’t actually use the property drawer. Would using the cached editor help with that? Some of this is new ground for me.
So I’m an idiot. first off to get this out of the way. CachedEditor was recommended simply as a performance reccomendation, it wouldn’t change the behaviour. Secondly the Extension Method creates an Editor, not a PropertyDrawer, polymorphically. so it should work just fine with Editor scripts, but not Drawers
and finally I remembered why I never did this in one of my projects, the simple gist was it was a total PITA. See unity basically blueprints what drawers will be used for which property of an object. if you class has an “AbstractClass” property then no matter what concrete class you actually assign to it Unity will load and use the “AbstractClassDrawer”. I remember couple months ago spending hours trying to get an AbstractEventBinder class to draw the concrete event types in the inspector to no avail.
Ultimately it means that you can’t just use PropertyDrawers Polymorphically, not without writing up a couple days worth of code to get it up and running, one idea is to create a static DrawerCache class (similar to unitys internal one) that stores a dictionary of concrete types to drawers, found through Reflection searching) and then the extension method can just lookup the drawer to use then. Or you can get an Asset from the asset store that already spent all their time getting that to work.
For now, You should be able to also write your PropertyDrawers as Editor classes and use that extension method.
So when I double click on a scriptable object, it creates an editor for that type an draws it in the inspector. The extension gets rid of the double click and just draws it in the inspector right?
So if I override on inspector gui I should be able to customize it?
From my research, the only alternatives are to save my scriptable object instances to asset files, or get rid of the polymorphism, keeping collections of each type by itself.
If you want it to be polymorphic then yeah you’ll need to create them as assets. and each ScriptableObject class will need to be saved into their own files of the same name as the class (so that unity can create the meta files it uses to index the classes). you could make them sub-assets to Example Behaviour, but I find it easier if you’d write the ScriptableObject like they are profiles and thus ExampleBehaviour will use the listed “profiles” when doing its logic.
Unity has trouble with polymorphic PropertyDrawers. Great for concrete fields, not so much for abstract fields. fortunately it has no problem at all at referencing a polymorphic object fields. so the least headache is to simply have the generic property render an ObjectField and just go directly to the object to work with its properties.
By profile, I simply mean a class that is just a data container which another class acts upon. the only example I can think of at the moment (relating to StateMachineBehaviours) is my SMB_ForceAnimationEvents class that I posted recently on the Animation forum.
in the example, the StateEvent class isn’t a ScriptableObject, but it doesn’t need to be and serves the same purpose. its a class that holds data that another class (the StateMachineBehaviour) will act upon.