Property Drawers
List of Property Drawers
Display attributes used for fields
Attribute | Applied to | Summary |
---|---|---|
CommentAttribute | Serialized field | Adds a comment to a field in the inspector. |
DummyAttribute | Serialized field | Marks a field as a dummy (usually to attach a property drawer unrelated to the field) |
HighlightAttribute | Serialized field | Highlights a field in the inspector. |
InspectorButtonAttribute | void method | Adds a button to the inspector that calls the method. |
InspectorFlagsAttribute | Enum serialized field | Draws an enum as a set of toggle buttons. |
LabelFieldAttribute | Serialized field | Marks a field as a label to use in arrays compound types. |
MinMaxRangeAttribute | Serialized field | Determines the range of MinMaxInt and MinMaxFloat fields. |
ReadOnlyAttribute | Serialized field | Marks a field as read-only. |
Validation attributes used for fields
Attribute | Applied to | Summary |
---|---|---|
ValidationAttribute | Serialized field | Base class for validation attributes. |
ValidateNotNullAttribute | Serialized class field | Validates that a field is not null. |
ValidateNotNegativeAttribute | Serialized number field | Validates that a field is not negative. |
ValidatePositiveAttribute | Serialized number field | Validates that a field is positive. |
ValidateRangeAttribute | Serialized number, MinMaxInt, or MinMaxFloat field | Validates that a field is within a range. |
ValidateMatchRegularExpressionAttribute | Serialized string field | Validates that a field matches a regular expression. |
Popup list attributes
Attribute | Applied to | Summary |
---|---|---|
PopupListAttribute | Serialized field | Base class for popup list attributes. |
StringPopupAttribute | Serialized field | Draws a popup list of strings. |
IntPopupAttribute | Serialized field | Draws a popup list of ints. |
ColorPopupAttribute | Serialized field | Draws a popup list of colors. |
TagPopupAttribute | Serialized field | Draws a popup list of tags. |
LayerPopupAttribute | Serialized field | Draws a popup list of layers. |
Types with custom property drawers
Type | Summary |
---|---|
InspectorList<T> | Render nicer lists in the inspector. (Not needed since Unity 2020.1.) |
Optional<T> | A struct that represents a value that may or may not be present. The value is only rendered in the inspector if the flag is true. |
Obsolete Types
Note
A fair amount of types have been made obsolete, for the main reason to have more consistent names, especially since we introduced a more comprehensive system for field validation.
Note
MinMaxRangeAttribute has not been made obsolete. Since it draws a different UI component altogether, it does not make sense to replace it with ValidateRangeAttribute. We may unify these in the future though.
Type | Replaced with |
---|---|
NonNegativeAttribute | ValidateNotNegativeAttribute |
PositiveAttribute | ValidatePositiveAttribute |
WarningIfNullAttribute | ValidateNotNullAttribute |
PropertyDrawerData
PropertyDrawerData handles settings used by property drawers.
Default Global Colors
Some property drawers use colors to highlight the fields. You can either specify the color in the attribute,
or change the global colors used in PropertyDrawerData, usually in a static constructor
of a class with the InitializeOnLoad
attribute.
Individual validation attributes can specify their own colors, and so can users of attributes.
Default Validation Policies
The booleans @Gamelogic.Extensions.PropertyDrawerData.ForceValid, WarnInInspector, and WarnInConsole controls how validation is executed for attributes derived from ValidationAttribute .
The defaults can be overridden by the attribute, or by the attribute user.
List Retrievers
Attributes derived from PopupListAttribute use a function (called a list retriever) that is called
to get the list used for the values of the popup. This allows you to compute the list from your game content, perhaps
set up in a scriptable object or a config file. You can register a list retriever using
@Gamelogic.Extensions.PropertyDrawerData.RegisterListRetriever, usually called from a static constructor of a class
with the InitializeOnLoad
attribute.
Defining your own popup list drawers
- Define an attribute that derives from PopupListAttribute. You only have to construct the base type with the desired constructors, depending on the types of retrieval you want to support.
using System.Linq;
namespace Gamelogic.Extensions.Samples
{
/// <summary>
/// An attribute used to mark a string field that should be drawn as a popup list of scene paths in the Unity editor.
/// </summary>
public class BuildScenePopupAttribute : PopupListAttribute
{
/// <summary>
/// Marks a string field that should be drawn as a popup list containing the paths of scenes included in the build settings.
/// </summary>
public BuildScenePopupAttribute()
: base(new PopupListData<string>(GetScenes))
{
}
#if UNITY_EDITOR
private static string[] GetScenes() => UnityEditor.EditorBuildSettings.scenes.Select(s => s.path).ToArray();
#else
private static string[] GetScenes() => System.Array.Empty<string>();
#endif
}
}
- Define a property drawer derived from PopupListDrawer. You only have to define the DrawField method.
using System.IO;
using Gamelogic.Extensions.Editor;
using UnityEditor;
using UnityEngine;
namespace Gamelogic.Extensions.Samples
{
/* This is an example of a custom property drawer that uses the PopupListDrawer to create a popup list of
scenes in the build settings.
We use the BuildScenePopupAttribute to mark the field that should be drawn as a popup list of scenes.
*/
public class BuildScenePropertyDrawer
{
[CustomPropertyDrawer(typeof(BuildScenePopupAttribute))]
public class BuildScenePopupPropertyDrawer : PopupListPropertyDrawer<string>
{
/// <summary>
/// Converts a string value into a <see cref="GUIContent"/> object for display in the popup list.
/// </summary>
/// <param name="value">The string value to convert.</param>
/// <returns>A <see cref="GUIContent"/> object representing the string value.</returns>
protected override GUIContent GetContent(string value)
=> new GUIContent( Path.GetFileNameWithoutExtension(value));
/// <summary>
/// Sets the string value of the serialized property based on the selected option in the popup list.
/// </summary>
/// <param name="property">The serialized property to set the value for.</param>
/// <param name="value">The string value to set.</param>
protected override void SetPropertyValue(SerializedProperty property, string value)
=> property.stringValue = value;
/// <summary>
/// Gets the current string value of the serialized property.
/// </summary>
/// <param name="property">The serialized property to get the value from.</param>
/// <returns>The current string value of the serialized property.</returns>
protected override string GetValue(SerializedProperty property) => property.stringValue;
}
}
}
Defining your own validation attributes.
- Define an attribute that derives from ValidationAttribute, and override the @Gamelogic.Extensions.ValidationAttribute.IsValid, and if the value can be forced valid, also the @Gamelogic.Extensions.ValidationAttribute.MakeValid method.
using Gamelogic.Extensions.Internal;
using Gamelogic.Extensions.Support;
namespace Gamelogic.Extensions.Samples
{
/// <summary>
/// Marks an integer field to indicate it should be a power of two.
/// </summary>
public class ValidatePowerOfTwoAttribute : ValidationAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="ValidatePowerOfTwoAttribute"/> class.
/// </summary>
public ValidatePowerOfTwoAttribute()
{
Message = "Value must be a power of two.";
Color = Branding.Aqua;
}
#if UNITY_EDITOR
/* You need to shield these methods with UNITY_EDITOR to avoid compilation errors in a build.
UnityEditor.SerializedProperty is not available at runtime, so these methods in the base
class are similarly shielded.
*/
/// <summary>
/// Returns whether the value is a power of two.
/// </summary>
/// <inheritdoc/>
[EditorOnly]
public override bool IsValid(UnityEditor.SerializedProperty property)
{
switch (property.propertyType)
{
case UnityEditor.SerializedPropertyType.Integer:
return property.intValue > 0 && (property.intValue & (property.intValue - 1)) == 0;
default:
return true;
}
}
/// <summary>
/// Returns the greatest power of two smaller or equal to `value`.
/// </summary>
/// <returns>The greatest power of two smaller or equal to `value`.</returns>
/// <inheritdoc/>
protected override void Constrain(UnityEditor.SerializedProperty property)
{
switch (property.propertyType)
{
case UnityEditor.SerializedPropertyType.Integer:
property.intValue = Constrain(property.intValue);
break;
}
}
private int Constrain(int intValue)
{
if (intValue <= 1)
{
return 1;
}
int constrainedValue = 1;
while (constrainedValue <= intValue / 2)
{
constrainedValue *= 2;
}
return constrainedValue;
}
#endif
}
}
Example
Here is an example of a data-driven content setup using a scriptable object:
using System;
using UnityEngine;
namespace Gamelogic.Extensions.Samples
{
[Serializable]
public class PowerUp
{
public string name;
public Color color;
}
/* This is an example of data that may be used in a game.
We use it to provide data for the property drawers (to make certain popups) in the ContentExample.
*/
[CreateAssetMenu]
public class ContentExample : ScriptableObject
{
/* These constants are used as keys to store the retriever functions we use to get the values
for the power ups and colors. The mapping is done in the PropertyDrawerDataInitializer, and
we the popups that use them are declared in the ContentExample.
*/
public const string PowerUpsRetrieverKey = "PowerUps";
public const string ColorsRetrieverKey = "Color";
public PowerUp[] powerUps;
public Color[] colors;
}
}
And here is how we can register list retrievers and set global colors:
using System;
using System.Linq;
using Gamelogic.Extensions.Editor.Internal;
using Gamelogic.Extensions.Support;
using UnityEditor;
using UnityEngine;
namespace Gamelogic.Extensions.Samples
{
/* This class initializes colors and list retrievers for the property drawers in the project.
It overrides some values in the PropertyDrawerData.
*/
[InitializeOnLoad]
public static class PropertyDrawerDataInitializer
{
static PropertyDrawerDataInitializer()
{
/* We set the default colors used by the Warning and Highlight property drawers.
*/
PropertyDrawerData.HighlightColor = Branding.Apple;
PropertyDrawerData.WarningColor = Branding.Lemon;
PropertyDrawerData.ForceValue = false;
/* Then we register two functions that will be used to retrieve the values for popups.
The functions use our content to get the values.
*/
var content = Assets
.FindByType<ContentExample>()
.FirstOrDefault();
PropertyDrawerData.RegisterValuesRetriever(ContentExample.PowerUpsRetrieverKey,
() => content == null ? Array.Empty<string>() : content.powerUps.Select(f => f.name)
);
PropertyDrawerData.RegisterValuesRetriever(ContentExample.ColorsRetrieverKey,
() => content == null ? Array.Empty<Color>() : content.colors
);
}
}
}
And here is a class using all the attributes and types with custom property drawers:
#pragma warning disable CS0169 // Field is never used. This file is to see the fields in the inspector only.
#pragma warning disable CS0414 // The private field is assigned but its value is never used
// ReSharper disable UnusedMember.Local
using UnityEngine;
using Gamelogic.Extensions.Internal;
using System;
using Gamelogic.Extensions.Support;
using JetBrains.Annotations;
using UnityEngine.Serialization;
namespace Gamelogic.Extensions.Samples
{
[Flags]
public enum MonsterState
{
IsHungry = 1,
IsThirsty = 2,
IsAngry = 4,
IsTired = 8
}
[Serializable]
public class MonsterData
{
public string name;
public string nickName;
public Color color;
}
// This class would work exactly the same in the inspector
// if it was extended from MonoBehaviour instead, except
// for the InspectorButton
// Some setup is done in the ThemeInitializer script
public class PropertyDrawerExample : GLMonoBehaviour
{
[Header("Display")]
[ReadOnly]
public string readonlyString = "Cannot change in inspector";
[Comment("This is a comment.")]
public string fieldWithComment = "This field has a comment";
[Highlight] // Uses default color in PropertyDrawerData set in PropertyDrawerDataInitializer.cs
[Space]
[SerializeField] private int highligtedInt;
[Highlight(Branding.Hex.Coral)]
[SerializeField] private int redInt;
[Space(order = 0)]
[Comment("Note the nickName is used for the labels of array items in the inspector", order = 1)]
[LabelField("nickName")]
[SerializeField] private MonsterData[] moreMonsters =
{
new MonsterData{ name = "Vampire", nickName = "Vamp", color = Utils.Blue },
new MonsterData{ name = "Werewolf", nickName = "Wolf", color = Utils.Red},
};
[FormerlySerializedAs("monsterState")]
[InspectorFlags]
[SerializeField] private MonsterState flags = MonsterState.IsAngry | MonsterState.IsHungry;
[Space(order = 0)]
[Separator(Branding.Hex.Coral, 5, order = 1)]
[Header("Validation", order = 2)]
[Comment("Can only be positive.", "Must be larger than 0.", order = 3)]
[ValidatePositive]
[SerializeField] private int positiveInt = 1;
[ValidatePositive]
[SerializeField] private float positiveFloat = 0.1f;
[Space(order = 0)]
[Comment("Cannot be negative", order = 1)]
[ValidateNotNegative]
[SerializeField] private int nonNegativeInt = 0;
[ValidateNotNegative(WarnInInspector = true, HexColor = Branding.Hex.Coral)]
[SerializeField] private float nonNegativeFloat = 0f;
[Space(order = 0)]
[Comment("Can only be between -1 and 1.", order = 1)]
[ValidateRange(-1, 1)]
[SerializeField] private float rangeFloat = 0f;
[Space(order = 0)]
[Comment("Can only be between -5 and 5.", order = 1)]
[ValidateRange(-5, 5)]
[SerializeField] private int rangeInt = 0;
[Space(order = 0)]
[Comment("Custom validation attribute: must be power of two.", order = 1)]
[ValidatePowerOfTwo]
[SerializeField] private int powerOfTwo = 1;
[Space(order = 0)]
[ValidateNotNull]
[SerializeField] private GameObject notNull;
[SerializeField, ValidateNotNull(WarnInConsole = true)]
private GameObject notNullWarningInConsole;
[SerializeField, ValidateNotNull(HexColor = Branding.Hex.Coral)]
private GameObject notNullCustomColor;
[ValidateNotNull] // No warning, since numbers are never null.
[SerializeField] private int notNullNumber = 10;
[Space(order = 0)]
[Comment(@"Must match: ^#?(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$", order = 1)]
[ValidateMatchRegularExpression(@"^#?(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$")]
[SerializeField] private string hexColor = "#fff";
[ValidateNotEmpty]
[SerializeField] private string nonEmptyString = "a";
[ValidateNotWhiteSpaceOrEmpty]
[SerializeField] private string nonWhiteSpaceEmptyString;
[Space(order = 0)]
[Separator(Branding.Hex.Coral, 5, order = 1)]
[Header("Special Types", order = 2)]
[SerializeField] private OptionalInt anOptionalInt = new OptionalInt
{
UseValue = true,
Value = 3
};
[Space(order = 0)]
[Comment("Value is between 0 and 1.", order = 1)]
[SerializeField] private MinMaxFloat minMaxFloatWithDefaultRange = new MinMaxFloat(0.25f, 0.75f);
[Space(order = 0)]
[Comment("Value is between 0 and 100.", order = 1)]
[MinMaxRange(0, 100)]
[SerializeField] private MinMaxInt minMaxIntPercentage = new MinMaxInt(50, 70);
[MinMaxRange(0, 100)]
[SerializeField] private MinMaxFloat minMaxFloatPercentage = new MinMaxFloat(50f, 70f);
[MinMaxRange(-100, 200)]
[SerializeField] private MinMaxFloat minMaxFloatPercentageWarn = new MinMaxFloat(50f, 70f);
[Space(order = 0)]
[Separator(Branding.Hex.Coral, 5, order = 1)]
[Header("Popups", order = 2)]
[IntPopup(new[]{5, 10, 15})]
[SerializeField] private int intPopupFromList = 5;
[StringPopup(new[] { "Vampire", "Werewolf" })]
[SerializeField] private string stringPopupFromList;
[StringPopup(ContentExample.PowerUpsRetrieverKey)]
[SerializeField] private string popupFromContent;
[ColorPopup(ContentExample.ColorsRetrieverKey)]
[SerializeField] private Color colorPopup;
[TagPopup]
[SerializeField] private string cameraTagPopup;
[LayerPopup]
[SerializeField] private string layerPopup;
[BuildScenePopup]
[SerializeField] private string buildScenePopup;
[Space(order = 0)]
[Separator(Branding.Hex.Coral, 5, order = 1)]
[Header("Inspector Buttons", order = 3)]
[Comment("Buttons show only if your object extends from GLMonoBehaviour or uses a custom editor that draws them",
order = 4)]
[Dummy, UsedImplicitly]
[SerializeField] private bool dummy; // Allows us to have decorators above the buttons.
//This will only show as a button if you extend from GLMonoBehaviour.
[InspectorButton]
public static void LogHello() => Debug.Log("Hello");
}
}