Game Development

Blog 808: The Trigger Editor (Part 2)

The saga of me wrangling Unity into my personal level editor continues. A few weeks ago I started using Polymorphic Serialisation for my trigger conditions and actions and — so far — this is actually holding up pretty well. There was one annoyance, however, in that adding an action using the list controls created an unsightly empty/null element that you… couldn’t actually do anything with.

Except now I’ve worked out how to do something with it, and it’s simplified a whole heap of other stuff!

The Trigger Editor (Part 2)

I was initially looking at my conversation editor (named ConEdit after the tool used for the same purpose in the Deus Ex SDK) when I turned down this rabbit-hole. ConEdit was one of the first times I went really deep into Unity editor tooling, with completely custom sets of controls for managing nodes on the dialogue tree and their links to each other. I was not particularly well-versed in Unity editor controls at this time, so it was very creaky — although I’ve just turned the trigger system upside down, the Trigger Editor window is more modern and so was mostly fine with those changes.

I’m not sure that’s entirely on me, mind you — in Unity there are tonnes and tonnes of different ways of drawing controls and applying their values to the underlying objects, and it provides basically no guidance on which ones are best for which situations. This is a great shame because I suspect one of Unity’s greatest features is this ability to meld the entire editor environment to suit your game’s particular needs, and now that I’ve been doing it a while, I’ve uncovered a lot of really satisfying approaches.

So while I was futzing around and getting annoyed with having to manually tie my lists into fully-fledged editor windows, I was reminded of “custom property drawers” — a more granular way to change how Unity presents a single editable field. Initially, I thought to myself: can I automatically have a selector button above every list of Trigger Conditions that I ever add? Then I don’t need to embed it in a fancy window to get the scaffolding.

This gambit failed because when you apply a property drawing attribute to an array, it does not apply to the array itself, but individually to every element within it… but that got me thinking. Could I, on the inspector trying to present a null entry, put my type selector button in there instead? And if it’s not null, draw the standard set of fields for the condition or action as if nothing’s any different?

As it turns out… Yes, actually, yes I can!

Polymorphic serialisation makes this a bit of an odd one, because Unity’s SerializedProperty only lets you set the value of a reference — not read it! So you can’t tell whether a reference field has a value or not here. It does, however, offer you the managedTypeName, which is an empty string when the field is null and has a type name when not. This gives us the switch we need.

The next thing you have to be careful about is overriding the GetPropertyHeight method, which is the critical thing my early forays into custom property drawers missed. As the name implies, it tells Unity how tall your property is going to be (including any children); get it wrong and at best you’ll get nothing, at worst you’ll get a huge munge of overlapping text. In this case, for a null that’s one line to contain the button, and for a non-null, it’s… however tall the actual condition might be. Thankfully, there is EditorGUIUtility.GetPropertyHeight which gives you access to the default height-calculator so you don’t have to reinvent the wheel. Job done!

[CustomPropertyDrawer(typeof(TriggerCondition))]
public class ConditionDrawer : PropertyDrawer
{
	public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
	{
		if (String.IsNullOrEmpty(property.managedReferenceFullTypename))
		{
			if (GUI.Button(position, "Pick Condition Type"))
			{
				TypeSelectorWindow<TriggerCondition> selectorWindow = new TypeSelectorWindow<TriggerCondition>();
				selectorWindow.Start(position, property);
				PopupWindow.Show(position, selectorWindow);
			}
		}
		else
		{
			EditorGUI.PropertyField(position, property, label, true);
		}
	}

	public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
	{
		if (String.IsNullOrEmpty(property.managedReferenceFullTypename))
		{
			return EditorGUIUtility.singleLineHeight;
		}
		else
		{
			return EditorGUI.GetPropertyHeight(property);
		}
	}
}

I should probably mention how my type selector works. It’s a little pop-up window that uses the TypeCache, a feature of the Unity editor that actually is really nice: it gives you convenient access to all the types in existence that descend from a given base type. In this case, that’s everything that subclasses TriggerCondition. I split them up into categories based on their topmost namespace for convenience, and once the user selects a type…

Making an instance of a type from its metadata with (T)Activator.CreateInstance(type) rather than just going new T() is a bit weird, though it seems to be reliable. Remember, kids: we’re in the editor so it’s okay to do a bit of Reflection.

Still need to do something about the trigger organiser on the left, and also the fact that I can’t actually DELETE a trigger without melting the entire window. Ah well.

So there we go — a combination of polymorphic serialisation and custom property drawers gives me a solid workflow with a lot less plumbing.

This is even better because it automatically applies to any TriggerCondition property I’ll ever add to anything; I don’t need to remember to put an extra attribute on, let alone build a load of scaffolding around it. Now, when it comes to the Trigger Editor window, all I need to do is tell Unity to EditorGUILayout.PropertyField my TriggerConditions array and it “just works”: I get that fully-functional reorderable list with fold-out instance elements and type selection for empty elements, without a load of extra wiring.

Which takes us all the way back to my intro paragraph, because this now works for the Trigger Editor window and in ConEdit — the lists of conditions that determine what node of a dialogue tree the player should start on, or what responses are available for a particular node, no longer need the same scaffolding copied over twice.

(Why didn’t Unity implement a little type-selector themselves? I now can’t imagine how else you’d interact with this polymorphic serialisation stuff. Ah well.)

A little less conversation, a little more action, baby…!

And you tell me...

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.