Unity: Build a better ReadOnlyAttribute

(Skip the read, here’s the repo. Otherwise, read on.)

I tend to use the Header attribute to break up my component values in the inspector. I’ll write code like this:

public class TestScript : MonoBehaviour {
	[Header("Inspector Values")]
	public bool someBool = true;
	public float someFloat = 1f;
	public int someInt = 30;

	[Header("Debug")]
	public bool debugBool = false;
	public float debugFloat = 0f;
	public int debugInt = 0;

	private void Update() {
		debugInt++;
		if(debugInt == someInt) {
			debugBool = !debugBool;
			debugInt = 0;
		}

		debugFloat += someFloat * Time.deltaTime;

		if(someBool && Mathf.Sin(debugFloat) > .95) {
			debugInt = 0;
		}
	}
}

That code will get you results like this:

Continue reading

Unity Tip: Getting the actual object from a custom property drawer

While writing some editor scripts for my current project, I ran into a small problem. I’m using a property drawer for a class like this.

[Serializable]
public class MyDataClass
{
    public void DoSomething()
    {
        // ...doing something
    }
}

And let’s setup a property drawer for this class.

[CustomPropertyDrawer(typeof(MyDataClass))]
public class MyDataClassDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (GUI.Button(position, "Do Something"))
        {
            MyDataClass myDataClass = fieldInfo.GetValue(property.serializedObject.targetObject) as MyDataClass;
            myDataClass.DoSomething(); // normally this works fine
        }
    }
}

Now, this usually works. For example, if you have a MonoBehaviour that includes a field of type MyDataClass, then this will get the job done. However, if you have a MonoBehaviour with a field of type MyDataClass[] then it won’t. The reason is that fieldInfo will refer to the array field of the parent object, and not the individual elements being drawn. In my case, I had MonoBehaviour classes that used MyDataClass for both single instances and in arrays. Not all is lost, however, as Unity provides us with some other useful info.

Let’s revise our approach above to get at the actual object being drawn whether it’s in an array or not. We know that if it’s not an array, we’re already good. So all we need to change is how we handle the case of the array. We can figure out if it’s an array by using plain old reflection. Note that we can’t use property.isArray, since property only refers to a single serialized object of type MyDataClass. But fieldInfo is reflection data that refers to the array field on the parent. If you think that’s a bit annoying, I agree.

[CustomPropertyDrawer(typeof(MyDataClass))]
public class MyDataClassDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (GUI.Button(position, "Do Something"))
        {
            var obj = fieldInfo.GetValue(property.serializedObject.targetObject);
            MyDataClass myDataClass = obj as MyDataClass;
            if (obj.GetType().IsArray)
            {
                // ...Need some magic here
            }

            myDataClass.DoSomething();
        }
    }
}

The SerializedProperty parameter passed into your OnGUI method contains a propertyPath field, which is a string that describes how you get from the parent object to the object currently being drawn. In the case of an array, the string will look something like “myDataClassArray.Array.Data[X]” (where X will be the index of the element currently being drawn). We can use a fairly sketchy (ha!) hack extracting the index (you could definitely make this more robust, but it serves our purposes here).

[CustomPropertyDrawer(typeof(MyDataClass))]
public class MyDataClassDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (GUI.Button(position, "Do Something"))
        {
            var obj = fieldInfo.GetValue(property.serializedObject.targetObject);
            MyDataClass myDataClass = obj as MyDataClass;
            if (obj.GetType().IsArray)
            {
                var index = Convert.ToInt32(new string(property.propertyPath.Where(c => char.IsDigit(c)).ToArray()));
                myDataClass= ((MyDataClass[])obj)[index];
            }

            myDataClass.DoSomething();
        }
    }
}

And that should do it. If you want to make this a bit more generic we could pull that logic out into a utility class of some sort and make it a static function. (Although it’s not an editor script exactly, you still probably want to drop it in an editor folder to prevent it from getting included into your build).

public class PropertyDrawerUtility
{
    public static T GetActualObjectForSerializedProperty<T>(FieldInfo fieldInfo, SerializedProperty property) where T : class
    {
        var obj = fieldInfo.GetValue(property.serializedObject.targetObject);
        if (obj == null) { return null; }

        T actualObject = null;
        if (obj.GetType().IsArray)
        {
            var index = Convert.ToInt32(new string(property.propertyPath.Where(c => char.IsDigit(c)).ToArray()));
            actualObject = ((T[])obj)[index];
        }
        else
        {
            actualObject = obj as T;
        }
        return actualObject;
    }
}

Now, we modify our original OnGUI method like so:

[CustomPropertyDrawer(typeof(MyDataClass))]
public class MyDataClassDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (GUI.Button(position, "Do Something"))
        {
            MyDataClass myDataClass = PropertyDrawerUtility.GetActualObjectForSerializedProperty<MyDataClass>(fieldInfo, property);
            myDataClass.DoSomething();
        }
    }
}

And we’re done. Hope you find this helpful.