Table of Contents

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.
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");
    }
}