Unity Property Drawers, Scene & Enum

Unity PropertyDrawers are incredibly useful they allow you to separate editor Ui extensions into reusable code. If you aren’t using property drawers or editor scripts you are missing out on a major benefit of Unity.

The Unity documentation does a good job explaining how to use them, I am going to expand on some points I felt got lost in the documentation, and extend 2 examples.

Property Drawers

PropertyAttribute’s define how the tags will be used in the rest of your code, what parameters can be passed to them and any preprocessing of the parameters. These parameters could be stored in the attribute to allow access when rendering the PropertyDrawer.

// New property attribute [Example]
public class ExampleAttribute : PropertyAttribute {

	// readonly as should only be set from constructor 
	// (world of pain if you try to modify at run time)
	public readonly bool isCustomRender = false;

	// This would be called using [Example]
	public ExampleAttribute() {
	}
	
	// This would be called using [Example(true)]
	public ExampleAttribute(bool customRender) {
		isCustomRender = customRender;
	}
}

PropertyDrawers’s define how the property should be rendered. This must be placed in a separate folder called Editor to work

// Define what attribute this property drawer is for.
[CustomPropertyDrawer(typeof(ExampleAttribute))]
public class ExampleDrawer : PropertyDrawer {

	// override the rendering of the Gui element for this property.
	public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {

		// Get the attribute to retrieve any values passed in to the attribute.
		ExampleAttribute exampleAttribute = (ExampleAttribute)attribute;

		if (exampleAttribute.isCustomRender) {
			// Start property rendering
			EditorGUI.BeginProperty(position, label, property);

			// Finish property rendering
			EditorGUI.EndProperty();
		} else {
			// Default render of property
			EditorGUI.PropertyField(position, property, label);
		}
	}
}

Enum Flags Drawer

The EnumFlag property drawer allows you to utilize the FlagsAttribute on enums properly in the inspector, similar to the way the Camera’s mask field is used. Allowing you to select multiple values in an Enum drop down. Camera Mask Field Example

First we create the attribute class to be used on fields we wish to show in the inspector.

using UnityEngine;
/// <summary>
/// Attribute declaration for EnumFlagDrawer, to be used on Enum with [Flags] set.
/// </summary>
public class EnumFlagAttribute : PropertyAttribute {
}

In a separate folder called Editor we create the PropertyDrawer to override how the property will be rendered.

using System;
using UnityEditor;
using UnityEngine;

/// <summary>
/// EnumFlag Drawer, enums tagged [EnumFlag] now show mask selection instead of singular enum.
/// <para/>
/// Use [SerializeField, EnumFlag] to display the mask selection window instead of a singular enum drop down box in the inspector.
/// This will allow you to select: Nothing, Everything, and multiple of your enum values, to be used as bit masks.
/// </summary>
[CustomPropertyDrawer(typeof(EnumFlagAttribute))]
public class EnumFlagDrawer : PropertyDrawer {

	/// <summary>
	/// Overrides Unity display of Enums tagged with [EnumFlag].
	/// </summary>
	public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
		// Grab the enum from the properties target.
		Enum targetEnum = (Enum)fieldInfo.GetValue(property.serializedObject.targetObject);

		// Start property rendering
		EditorGUI.BeginProperty(position, label, property);
		// Use Unity's Flags field to render the enum instead of the default mask.
		// This allows you to select multiple options.
		Enum enumNew = EditorGUI.EnumFlagsField(position, label, targetEnum);
		// Convert back to an int value to represent the mask fields, as that is how its stored on the serialized property.
		property.intValue = (int)Convert.ChangeType(enumNew, targetEnum.GetType());
		// Finish property rendering
		EditorGUI.EndProperty();

	}
}

Scene Drawer

The Scene property drawer overrides the rendering of a string field to show a selection window of available scenes. Scene Property Drawer Scene Property Drawer Selection It will show an error if the selected scene is not in the project BuildSettings Scenes In Build, or if it is not assigned to a string property.

The same as before we create the attribute class to be used on fields we wish to show in the inspector.

using UnityEngine;
/// <summary>
/// Attribute declaration for SceneDrawer, to be used on strings.
/// </summary>
public class SceneAttribute : PropertyAttribute {
}

Again in a separate folder called Editor we create the PropertyDrawer to override how the property will be rendered.

using UnityEditor;
using UnityEngine;

/// <summary>
/// Scene Drawer, strings tagged [Scene] now show Scene selection instead of strings.
/// <para/>
/// Use [SerializeField, Scene] to display theScene selection window instead of a string input box.
/// This property drawer will also check if the selected scene has already been added to 'Scenes in the Build'
/// if not it will warn the user.
/// Set strings to "" for the property drawer to correctly select 'none' without display error logs.
/// </summary>
[CustomPropertyDrawer(typeof(SceneAttribute))]
public class SceneDrawer : PropertyDrawer {

	/// <summary>
	/// Overrides Unity display of scenes tagged with [Scene].
	/// </summary>
	public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {

		// Only override rendering on properties with type of string (otherwise it doesn't work).
		if (property.propertyType == SerializedPropertyType.String) {
			// Get currently selected object (generated from previous string value).
			SceneAsset sceneObject = GetSceneObject(property.stringValue);
			// Render a SceneAsset object field, with the previous string value, shown as a SceneAsset.
			Object scene = EditorGUI.ObjectField(position, label, sceneObject, typeof(SceneAsset), true);

			if (scene == null) {
				// We get nulls if the scene asset isn't found so wipe the string value.
				property.stringValue = "";

				// If the scene name doesn't match the property they have changed it
				// either by drag n drop or the object picker.
			} else if (scene.name != property.stringValue) {
				// Convert the Object to a SceneAsset.
				SceneAsset sceneObj = GetSceneObject(scene.name);
				// If its a valid scene asset we use it, otherwise assume select invalid and ignore.
				if (sceneObj != null) {
					property.stringValue = scene.name;
				}
			}
		} else {
			EditorGUI.LabelField(position, label.text, "Use [Scene] with strings.");
		}
	}

	/// <summary>
	/// Retrieve the scene string from the editor build settings, returns null if not found.
	/// 
	/// </summary>
	/// <param name="sceneObjectName"> The asset name path of the scene object. </param>
	/// <returns> The SceneAsset or null if not found. </returns>
	protected SceneAsset GetSceneObject(string sceneObjectName) {
		if (string.IsNullOrEmpty(sceneObjectName)) {
			// Early exit as know it will return null when string null or empty.
			return null;
		}
		// Iterate over the scenes in the build settings
		foreach (EditorBuildSettingsScene editorScene in EditorBuildSettings.scenes) {
			// We found the scene object's name in the editor scene's path.
			// This assumes that a scene will not be named exactly the same as a parent folder & a duplicate of another scene.
			if (editorScene.path.IndexOf(sceneObjectName) != -1) {
				return AssetDatabase.LoadAssetAtPath(editorScene.path, typeof(SceneAsset)) as SceneAsset;
			}
		}
		// Scene not found, assume it isn't in the build settings.
		Debug.LogWarning("Scene [" + sceneObjectName + "] cannot be used. Add this scene to the 'Scenes in the Build' in build settings.");
		return null;
	}
}

Related

comments powered by Disqus