Part 4: Disjunction Junction, What’s your Function
We’ve got our Predicate property drawer in place, now it’s time to work with the Disjunction property drawer. At this point, you could actually skip the rest of this tutorial and have a fully operational inspector, relying on Unity’s built in array/list handling methods to manage the rest of the Condition. This is fine, but I’m sure some of you have noticed that sometimes Unity’s List and Array handling can be a bit buggy. This next lesson will give us a chance to implement a custom inspector for handling our predicates, and learn the best practices for crawling through a list or array of unknown size in a PropertyDrawer…
We’re going to start by creating, in the same folder as the PredicatePropertyDrawer, a new cs file, DisjuncitonPropertyDrawer.cs. Do the same sort of setup you did with the PredicatePropertyDrawer
using UnityEngine;
using UnityEditor;
namespace GameDevTV.Utils.Editor;
{
[CustomPropertyDrawer(typeof(Condition.Disjunction))]
public class DisjunctionPropertyDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
}
}
}
We’re going to start out by getting the size of our GetPropertyHeight. We’ll need this sooner than later, since predicates take several lines each to draw. For this, we’ll override GetPropertyHeight()
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
float result=0;
float propHeight = EditorGUIUtility.singleLineHeight;
return result+propHeight;
}
Before I go much further, in the previous section, PropertyDrawer, I used EditorGUI.GetPropertyHeight() on the Epredicate property to get our property height. I confess to forgetting that this number can more easily be obtained with EditorGUIUtility.singleLineHeight. This will give you the standard size of one line. I had to recall this property for the disjuction editor because our editor only contains one property, an array of Predicates, and this value can be variable, depending on the predicate.
I’ve set the method up with my sandwitch of initializing a result and returning that result. The rest of what we’ll be doing is between the statements.
IEnumerator enumerator = property.FindPropertyRelative("or").GetEnumerator();
Since or is a Predicate[]
, we’re going to need to iterate over the array. One might be tempted to use a for loop, but I ran into difficulties when the array had zero elements. This actually affords us an opportunity to iterate over the array in another way. It happens that you can get an enumerator from a serialized property and iterate over the values. Bear in mind that the feature set of this enumerator is a bit restricted compared to an IEnumerable, array or a list.
while(enumerator.MoveNext())
{
SerializedProperty p = enumerator.Current as SerializedProperty;
result+=EditorGUI.GetPropertyHeight(p) +propHeight;
}
MoveNext() looks in the container to see if there is another item. If there is, it sets the Current pointer to that item. We take that enumerator.Current (which is an object) and convert it to a SerializedProperty.
Next, we add the GetPropertyHeight() of the property and add it to result, + one line for the buttons that will allow us to move the element up/down the array or remove it altogether.
You might be wondering, at this point, where does EditorGUI.GetPropertyHeight() get the height for the property? Quite simple, since the property is a Predicate, it asks the PredicatePropertyDrawer.GetPropertyHeight(). Since we’re passing in the specific property, the propertydrawer is able to calculate the correct height. Once there are no more elements in the array, MoveNext() will return false and the while loop will exit
So now GetPropertyHeight looks like this:
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
float result = 0;
float propHeight = EditorGUIUtility.singleLineHeight;
IEnumerator enumerator = property.FindPropertyRelative("or").GetEnumerator();
while (enumerator.MoveNext())
{
SerializedProperty p = enumerator.Current as SerializedProperty;
result += EditorGUI.GetPropertyHeight(p)+propHeight;
}
return result+propHeight;
}
That last propHeight will be for our Add Predicate button.
With our GetPropertyHeight set up, it’s time to start drawing the property:
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
SerializedProperty or = property.FindPropertyRelative("or");
float propHeight = EditorGUIUtility.singleLineHeight;
var enumerator = or.GetEnumerator();
while (enumerator.MoveNext())
{
SerializedProperty p = enumerator.Current as SerializedProperty;
position.height = EditorGUI.GetPropertyHeight(p);
EditorGUI.PropertyField(position, p);
position.y += position.height;
position.height = propHeight;
}
}
At this point, we’re getting the standard line size, and we’re getting the enumerator we used in the GetPropertyHeight method. This time, we’re going to set the position’s height to the size of the property and draw each element using the EditorGUI.PropertyField() method. In this case, PropertyField will find the property drawer we wrote for the Predicate and draw it, just like as if it was a built in component. By now you should be starting to see just how powerful property drawers can be.
Here’s our new inspector:
Of course, we have issues… there’s a lot of extra space, since we added that space in when we did the calculations for GetPropertyHeight, and there is no way to add or remove elements. Plus, if you look at the last element, you’ll see it’s two Predicates smashed together. That’s not terribly readable.
Let’s start by adding functionality to add a record
at the end of the OnGUI method, add this:
if (GUI.Button(position, "Add Predicate"))
{
or.InsertArrayElementAtIndex(idx);
}
This draws a button at the current position, and if the button is pressed, then it inserts an array element. Of course, we haven’t defined idx. Before the while loop, add this line:
int idx=0;
and at the end of the while loop add this line:
int idx++;
Now you should have an Add Predicate button after each set of or
But we still want to be able to reorder and remove the items…
We’re going to need some extra rects and integer variables to make this happen…
Let’s start with the rects… all of this should come before var enumerator = or.GetEnumerator()
Rect upPosition = position;
upPosition.width -= EditorGUIUtility.labelWidth;
upPosition.x = position.xMax - upPosition.width;
upPosition.width /= 3.0f;
upPosition.height = propHeight;
Rect downPosition = upPosition;
downPosition.x += upPosition.width;
Rect deletePosition = upPosition;
deletePosition.x = position.xMax - deletePosition.width;
int itemToRemove = -1;
int itemToMoveUp = -1;
int itemToMoveDown = -1;
There’s a lot going on here. In short, we’re creating three rects, one for the up arrow, one for the down arrow, and one for the delete button. We start by copying the position rect. We deduct the label spacing from the new rect, and divide the width by 1/3rd. We then set the height to be a single line height. Then we clone this rect and move it over next to the first rect, and clone it again moving it to the far right edge of the property. Finally, we create three ints as placeholders for if any of the buttons were pressed.
Now just before idx++; in our while loop, add these lines of code, which will give us our remove and up buttons:
upPosition.y = deletePosition.y = downPosition.y = position.y;
if (GUI.Button(deletePosition, "Remove")) itemToRemove = idx;
if (idx > 0 && GUI.Button(upPosition, "^")) itemToMoveUp = idx;
position.y+=propHeight;
The first line sets the y position of our buttons. We’re employing a C# trick to assign all three values to position.y.
Next, we draw a button, setting itemToRemove to idx if this button is clicked.
if idx is > 0 We draw another button, setting itemToMoveUp is clicked.
We can’t draw our down button yet, though, because we don’t know if there are more elements until we get into the next iteration in the while loop. We’ll get to that later.
Finally, we need two more if statements, outside of the while loop, before our button draw for the Add Predicate button.
if (itemToRemove >= 0)
{
or.DeleteArrayElementAtIndex(itemToRemove);
}
if (itemToMoveUp >= 0)
{
or.MoveArrayElement(itemToMoveUp, itemToMoveUp - 1);
}
Basically, these check to see if we’ve cached an index for deleting or moving. If we’re deleting, we just DeleteArrayElementAtIndex(itemToRemove). If we’re moving up, then we MoveArrayElement from the itemToMoveUp index to the itemToMoveUp index-1. Remember that if(idx>0 && statement before drawing the move up button? This ensures that the button is only drawn on the [1] or greater element, so we never have to worry about trying to move 0 to 0-1.
We now have remove and up (^) buttons on each predicate. The ^ button only exists where there is more than one or clause in the disjunction, like the bottom most element.
Still, we need two more tidbits to finish this thing off… we need a move down button on OR elements with more than one predicate, and we need something to visually remind us in these cases that these are OR conditions… We’ll manage this at the top of the While loop:
if (idx > 0)
{
if (GUI.Button(downPosition, "v")) itemToMoveDown = idx - 1;
EditorGUI.DropShadowLabel(position, "-------OR-------", style);
position.y += propHeight;
}
There’s a reason this is at the beginning of the while loop. We don’t know if we need the OR separator or a Move down button until we move to the next element. If the idx is zero, then we don’t need to draw an OR or a down button. If we do, we draw the down button for the previous element and then an OR.
You’ll notice I’ve included a style on that label. We’ll define that at the beginning of the ONGui method:
GUIStyle style = new GUIStyle(EditorStyles.label);
style.alignment = TextAnchor.MiddleCenter;
This creates a new style, copying from the normal label style settings in EditorStyles. We then set the alignment to the center so the ------OR------- will be centered. To use the style, just insert it as the last parameter of a Label or DropShadowLabel.
One more thing to make the down button work, after our if statements for the remove and up buttons:
if (itemToMoveDown >= 0)
{
or.MoveArrayElement(itemToMoveDown, itemToMoveDown + 1);
}
And now our inspector should look like this:
You should be able to add predicates to an or
, remove them, and move them up and down the chain with the ^ and v buttons.
In the next lesson, we’ll round this out by drawing the condition property drawer, which will give us an improved handling of the array of disjunctions,
Complete DisjunctionPropertyDr
Complete DisjunctionPropertyDrawer.cs
using System.Collections;
using UnityEngine;
using UnityEditor;
namespace GameDevTV.Utils.Editor
{
[CustomPropertyDrawer(typeof(Condition.Disjunction))]
public class DisjunctionPropertyDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
GUIStyle style = new GUIStyle(EditorStyles.label);
style.alignment = TextAnchor.MiddleCenter;
SerializedProperty or = property.FindPropertyRelative("or");
float propHeight = EditorGUIUtility.singleLineHeight;
Rect upPosition = position;
upPosition.width -= EditorGUIUtility.labelWidth;
upPosition.x = position.xMax - upPosition.width;
upPosition.width /= 3.0f;
upPosition.height = propHeight;
Rect downPosition = upPosition;
downPosition.x += upPosition.width;
Rect deletePosition = upPosition;
deletePosition.x = position.xMax - deletePosition.width;
var enumerator = or.GetEnumerator();
int idx = 0;
int itemToRemove = -1;
int itemToMoveUp = -1;
int itemToMoveDown = -1;
while (enumerator.MoveNext())
{
if (idx > 0)
{
if (GUI.Button(downPosition, "v")) itemToMoveDown = idx - 1;
EditorGUI.DropShadowLabel(position, "-------OR-------", style);
position.y += propHeight;
}
SerializedProperty p = enumerator.Current as SerializedProperty;
position.height = EditorGUI.GetPropertyHeight(p);
EditorGUI.PropertyField(position, p);
position.y += position.height;
position.height = propHeight;
upPosition.y = deletePosition.y = downPosition.y = position.y;
if (GUI.Button(deletePosition, "Remove")) itemToRemove = idx;
if (idx > 0 && GUI.Button(upPosition, "^")) itemToMoveUp = idx;
position.y += propHeight;
idx++;
}
if (itemToRemove >= 0)
{
or.DeleteArrayElementAtIndex(itemToRemove);
}
if (itemToMoveUp >= 0)
{
or.MoveArrayElement(itemToMoveUp, itemToMoveUp - 1);
}
if (itemToMoveDown >= 0)
{
or.MoveArrayElement(itemToMoveDown, itemToMoveDown + 1);
}
if (GUI.Button(position, "Add Predicate"))
{
or.InsertArrayElementAtIndex(idx);
}
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
float result = 0;
float propHeight = EditorGUIUtility.singleLineHeight;
IEnumerator enumerator = property.FindPropertyRelative("or").GetEnumerator();
bool multiple = false;
while (enumerator.MoveNext())
{
SerializedProperty p = enumerator.Current as SerializedProperty;
result += EditorGUI.GetPropertyHeight(p)+propHeight;
if (multiple) result += propHeight;
multiple = true;
}
return result+propHeight*1.5f;
}
}
}