devlog

Broken Ice, Enums Pt. 2

28th July 2017

549

This is the second devlog on working with Enums in Unity—Pt. 1 can be found here, and covers the basics of setting up Enums.  This post will delve deeper into using flagged enums in Unity, picking up right where we left off from declaring enums.  (Flagged enums, if you’re just joining in, are a convenient way to group multiple labeled values—or options—together into a single variable.)  This post will continue with making a PropertyDrawer for enums: wrapping up concepts of serializing (saving the value for) flagged enums, writing a proper “drawer” for flagged enums, and the general ins-and-outs of custom Inspectors in Unity.

This post does have some corresponding goodies, which you can download from our GitHub here.  You can either download the scripts directly from the Enum directory, or download the assetPackage and follow along in Unity.  These both contain ready-to-use code you can drop into your own projects, as well as examples that you can use to follow-along with this devlog.

We’ll be continuing with the Vehicles flagged enum we crated in Pt. 1 (shown below).  And with that introduction, let’s get started.

Pt. 2: Ease of Use in Inspector—PropertyDrawers

So now that we have our Vehicles flagged enum, how do we make the damned thing work in the inspector?  If you try it now, you’ll see that it still holds a single value—Unity isn’t treating it like a flagged enum.  Turns out, Unity has no built-in method for handling flagged enums.  Shame.  To create a custom inspector for a type, we need to define a PropertyDrawer.  I’m going to skip over how to make a PropertyDrawer, since there are tutorials available here and here.  Instead, I’ll quickly cover the basics, then we’ll learn to create them through trial and error.

PropertyDrawers Basics

PropertyDrawers are the counterpart to Editors in Unity.  While Editors specifically work on MonoBehaviours—the actual scripts you attach to GameObjects—PropertyDrawers work on the parts of code you declare as fields.  Editors also replace the entire Inspector, while PropertyDrawers only replace a single field (and as such, can be used by Editor scripts).  PropertyDrawers can be created for two different uses:

  • Hard-coded Type version: replacing the Inspector for every instance of a Type
  • PropertyAttribute version: replacing the Inspector for fields using an Attribute (declared in brackets above a variable, such as [Range])

We’ll cover both versions of PropertyDrawers, starting with the first.

v1: Vehicles Specific

Our first Drawer will be hard-coded specifically for our Vehicles example.  We simply need to create a drawer that allows selection of each of our Vehicles, and assigns the value back to our variable.  The code is quite short:

And since this is hard-coded to work on all Vehicles, we don’t need to do anything special in our example MonoBehaviour to get the PropertyDrawer to work.  But while this does the job (kinda, as we’ll see in Drawer v2), it’s not very handy.  No developer is going to make a custom PropertyDrawer for every flagged enum.  So, let’s make a version that works for all flagged enums, instead.

 v2: Any Flagged Enums

To apply to all flagged enums instead of just Vehicles, this next Drawer will be the second version of PropertyDrawer: one that works using a PropertyAttribute.  This allows it to extend to whatever flagged enum type we want, just by adding the Attribute above our variable—using the bracket notation for Attributes.  This second version also takes precedence over the first, so we don’t have to worry about deleting out v1 for this v2 to work.

This time around, we’ll need to make two classes: our PropertyAttribute (which we’ll name EnumFlagsBrokenAttribute) and our PropertyDrawer (which we’ll name EnumFlagsBrokenDrawer).  Doesn’t sound too hopeful, does it?  The Attribute and Drawer look like this:

Now in our example MonoBehaviour, pop [EnumFlagsBroken] above a Vehicles variable, and we’re good to go:

As an aside: you’ll notice the class ends with Attribute, but when using it we can omit it.  This is a handy feature in C#, and should be a standard we all stick to.

Okay, that’s kinda cool.  You may notice that we’re using MaskField instead of the EnumMaskField from before—turns out, all the EnumMaskField, EnumMaskPopup, and EnumPopup overloads require an instance of Enum, which we no longer have.  We can’t directly convert an int to Enum, either (since we don’t know the actual enum Type), so there’s not much we can do outside of using the standard MaskField option.  But even Valve got this far, so it must be done right!  Right?

Bug 1: No Knowledge of Enum

There are a few bugs and issues with this solution.  First, try to select just Pickup.  We’d expect BigAssTruck to get selected as well, right?  Odd…  it is the same value, after all.  Now if you click the “Test Values” button in our example MonoBehaviour, you’d expect to see “Pickup”, or at the very least “BigAssTruck” debugging to the console.  Turns out it, displays “SUV.”  That sucks.  Since we’re using MaskField, it has no knowledge of the actual values behind the enum—the only input it receives is an array of the enum names by string.  So, that’s not terribly unexpected, and can be worked around—as long as we don’t skip, duplicate, or have combined entry values (e.g. Car = Convertible | Hatchback | Compact to indicate that a car is any of those three options).  Too bad our Vehicles example falls under one of these problem areas.

Bug 2: Multi-Object Selection

The next bug, though, is far more severe.  Let’s give this a shot:

  1. Set “Enum Example” to using “SUV” only for commonDrawerBroken.
  2. Duplicate the “Enum Example” GameObject to make “Enum Example (1)”.
  3. Set “Enum Example (1)” to using Compact only for commonDrawerBroken.
  4. Select both Enum Example GameObjects (hold control to allow multi-object selection).

If you're not following along in Unity, what we just did was:

You’ll notice that commonDrawerBroken now displays “Compact”, instead of something telling us that our selections have different values.  Selecting the Enum Examples individually cements the horror: we lost all our data on the original Enum Example.