Hiding or Disabling inspector properties using PropertyDrawers within Unity 5

Blog_Unity5PropertyDrawers01_2015_11_29_Example05When working with unity and larger scripts that provide a lot of adjustable parameters, the inspector can quickly turn into a very unmanageable wall of text and entry fields.

So lets take a look at what can be done to make inspector navigation a much more enjoyable experience without having to write a custom inspector layout for your script.

In this post I quickly go over the use of HeaderAttributes to help you keep the inspector clean and easy to work with. And finally I explain how you can hide or disable inspector properties based on a bool field within your script.

Improving Basic Readability

Lets take a look at the following example inspector window.

Blog_Unity5PropertyDrawers01_2015_11_29_Example01

Notice that with even a small script like this it can already be difficult to get a good idea of what is going on.

What we would like is the ability to group our fields in a way that makes sense for the designer. Making it very clear what the role is of the different variables within the functionality of my script. This can be done with HeaderAttributes.

Simply add the attribute in front of  your public field and you are good to go. A simple, quick and super effective way to instantly increase the readability of your script within Unity’s editor.

    [Header("Auto Aim")]
    public bool EnableAutoAim = false;

Blog_Unity5PropertyDrawers01_2015_11_29_Example02

Unity provides a series of additional attributes that can help you in managing the look of your script within the inspector and how the user can interact with them. All without having to write special inspector specifically for this script.

Some useful and commonly used attributes to look into are the space, tooltip and range attributes.

Hide or Disable Fields based on user input

Using HeaderAttributes is all shiny and cool but what if we want to help the designer even further?

In this example most of the properties under the Auto Aim category will only be relevant when EnableAutoAim is set to true.
Say that, to avoid confusion and to keep our inspector nice and clean, we want to hide all the properties that are directly related to the EnableAutoAim if this property is set to false. Or instead of hiding them, we want to at least disable them, preventing user input.

Blog_Unity5PropertyDrawers01_2015_11_29_Example07

To make this possible we need to create a custom attribute and matching PropertyDrawer.
I’m not going to go into the details on how PropertyDrawers and Attributes fully work. For that I will point you towards https://unity3d.com/learn/tutorials/modules/intermediate/live-training-archive/property-drawers-custom-inspectors.

But I will go over the logic and setup of this specific PropertyDrawer and attribute.
Lets get started 🙂 .

Step 1: Create the scripts

As mentioned we are going to need 2 scripts: ConditionalHideAttribute and ConditionalHidePropertyDrawer.
Important to note is that the ConditionalHidePropertyDrawer needs to be placed inside an Editor folder within the project and the ConditionalHideAttribute scripts needs to be located outside this folder. Otherwise you will not be able to find and use the attribute.

Blog_Unity5PropertyDrawers01_2015_11_29_Example08

Step 2: ConditionalHideAttribute

First we create our Attribute. We want to be able to define a bool Field that will be in control of the hiding/enabling of the inspector properties and we also want to be able to choose between the hide and enable options.

The  ConditionalHideAttribute inherits from the PropertyAttribute class and is nothing more than a simple data class with some constructors. The main purpose of this class is to provide additional data that will be used within the PropertyDrawer. If you want to add extra options/ parameters to this attribute, this is where you would add them.

The AttributeUsage attributes used at the top of the ConditionalHideAttribute class control where you will be able to use this attribute.

using UnityEngine;
using System;
using System.Collections;

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property |
    AttributeTargets.Class | AttributeTargets.Struct, Inherited = true)]
public class ConditionalHideAttribute : PropertyAttribute
{
    //The name of the bool field that will be in control
    public string ConditionalSourceField = "";
    //TRUE = Hide in inspector / FALSE = Disable in inspector 
    public bool HideInInspector = false;

    public ConditionalHideAttribute(string conditionalSourceField)
    {
        this.ConditionalSourceField = conditionalSourceField;
        this.HideInInspector = false;
    }

    public ConditionalHideAttribute(string conditionalSourceField, bool hideInInspector)
    {
        this.ConditionalSourceField = conditionalSourceField;
        this.HideInInspector = hideInInspector;
    }
}

Step 3 : ConditionalHidePropertyDrawer

In the PropertyDrawer we need to do a couple of things.

  • When Unity wants to draw the property in the inspector we need to:
    • Check the parameters that we used in our custom attribute
    • Hide and/or disable the property that is being drawn based on the attribute parameters
  public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        //get the attribute data
        ConditionalHideAttribute condHAtt = (ConditionalHideAttribute)attribute;
        //check if the propery we want to draw should be enabled
        bool enabled = GetConditionalHideAttributeResult(condHAtt, property);

        //Enable/disable the property
        bool wasEnabled = GUI.enabled;
        GUI.enabled = enabled;

        //Check if we should draw the property
        if (!condHAtt.HideInInspector || enabled)
        {
            EditorGUI.PropertyField(position, property, label, true);
        }

        //Ensure that the next property that is being drawn uses the correct settings
        GUI.enabled = wasEnabled;
    }

 

In order to check if the property should be enabled or not we call GetConditionalHideAttributeResult.

    private bool GetConditionalHideAttributeResult(ConditionalHideAttribute condHAtt, SerializedProperty property)
    {
        bool enabled = true;
        //Look for the sourcefield within the object that the property belongs to
         string propertyPath = property.propertyPath; //returns the property path of the property we want to apply the attribute to
        string conditionPath = propertyPath.Replace(property.name, condHAtt.ConditionalSourceField); //changes the path to the conditionalsource property path
        SerializedProperty sourcePropertyValue = property.serializedObject.FindProperty(conditionPath);

        if (sourcePropertyValue != null)
        {
            enabled = sourcePropertyValue.boolValue;
        }
        else
        {
            Debug.LogWarning("Attempting to use a ConditionalHideAttribute but no matching SourcePropertyValue found in object: " + condHAtt.ConditionalSourceField);
        }

        return enabled;
    }

 

  • Calculate the height of our property so that (when the property needs to be hidden) the following properties that are being drawn don’t overlap
  public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        ConditionalHideAttribute condHAtt = (ConditionalHideAttribute)attribute;
        bool enabled = GetConditionalHideAttributeResult(condHAtt, property);

        if (!condHAtt.HideInInspector || enabled)
        {
            return EditorGUI.GetPropertyHeight(property, label);
        }
        else
        {
            //The property is not being drawn
            //We want to undo the spacing added before and after the property
            return -EditorGUIUtility.standardVerticalSpacing;
        }
    }

 

The complete ConditionalHidePropertyDrawer script then looks as follows:

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(ConditionalHideAttribute))]
using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(ConditionalHideAttribute))]
public class ConditionalHidePropertyDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        ConditionalHideAttribute condHAtt = (ConditionalHideAttribute)attribute;
        bool enabled = GetConditionalHideAttributeResult(condHAtt, property);

        bool wasEnabled = GUI.enabled;
        GUI.enabled = enabled;
        if (!condHAtt.HideInInspector || enabled)
        {
            EditorGUI.PropertyField(position, property, label, true);
        }

        GUI.enabled = wasEnabled;
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        ConditionalHideAttribute condHAtt = (ConditionalHideAttribute)attribute;
        bool enabled = GetConditionalHideAttributeResult(condHAtt, property);

        if (!condHAtt.HideInInspector || enabled)
        {
            return EditorGUI.GetPropertyHeight(property, label);
        }
        else
        {
            return -EditorGUIUtility.standardVerticalSpacing;
        }
    }

    private bool GetConditionalHideAttributeResult(ConditionalHideAttribute condHAtt, SerializedProperty property)
    {
        bool enabled = true;
        string propertyPath = property.propertyPath; //returns the property path of the property we want to apply the attribute to
        string conditionPath = propertyPath.Replace(property.name, condHAtt.ConditionalSourceField); //changes the path to the conditionalsource property path
        SerializedProperty sourcePropertyValue = property.serializedObject.FindProperty(conditionPath);

        if (sourcePropertyValue != null)
        {
            enabled = sourcePropertyValue.boolValue;
        }
        else
        {
            Debug.LogWarning("Attempting to use a ConditionalHideAttribute but no matching SourcePropertyValue found in object: " + condHAtt.ConditionalSourceField);
        }

        return enabled;
    }
}

Step 4: How to use

Using our newly created ConditionalHideAttribute is fairly straightforward and works much like the HeaderAttribute.

    [Header("Auto Aim")]
    public bool EnableAutoAim = false;

    [ConditionalHide("EnableAutoAim", true)]
    public float Range = 0.0f;
    [ConditionalHide("EnableAutoAim", true)]
    public bool AimAtClosestTarget = true; 
    [ConditionalHide("EnableAutoAim", true)]
    public DemoClass ExampleDataClass = new DemoClass();
    [ConditionalHide("EnableAutoAim", true)]
    public AnimationCurve AimOffsetCurve = new AnimationCurve();

Leaving out the second parameter or setting it to false draws the property using the enable/disable method.

    [Header("Resources")]
    public bool ConsumeResources = true;

    [ConditionalHide("ConsumeResources")]
    public bool DestroyOnResourcesDepleted = true;
    [ConditionalHide("ConsumeResources")]
    public float ResourceAmount = 100.0f;
    [ConditionalHide("ConsumeResources")]
    public DemoClass AnotherExampleDataClass = new DemoClass();

Some Caveats

This being Unity after all there are a few situations where this custom attribute will fail.
The first situation being Lists and arrays.
In those cases the property drawer will be applied to the content of the list but not on the list itself… .
You will still be able to adjust the number of items as well.
However when the List is part of a Seriazable class everything will behave as expected.

Why is this you ask? Because Unity, that’s why.

Blog_Unity5PropertyDrawers01_2015_11_29_Example09

Blog_Unity5PropertyDrawers01_2015_11_29_Example10
Another situation where the use of this attribute will fail is when being combined with different property drawers that change the way the property itself is being drawn such as the TextAreaAttribute.
This makes sense as this attribute performs his own OnGUI drawing and GetPropertyHeight calculation of the same property. The attribute that is called last will be the attribute that determines what is being drawn in this case.

Update 01 [15/03/2016]

Added support for 2 bools instead of only 1.
You can use this option by simply adding the second ConditionalSourceField as parameter.

    [ConditionalHide("Condition01", ConditionalSourceField2 = "Condition02")]

Update 02 [13/04/2016]

Based on a suggestion by @VeevTSG via Twitter I have added support for object references to be used as a condition.
You can easily extend the code to add additional condition checks such as integers, floats, strings, etc.

Update 03 [17/12/2016]

Added support for conditional property hiding in nested/encapsulated serializable objects that are used as inspector properties.
Thanks CrashOverride for the great addition in the comments!!
The downloadable file and article above have both been updated with the new addition.

That’s it

I hope this post was off some use and be sure to let me know if it was :).
You can download the 2 script files directly using the following link: ConditionalHideAttribute.rar

23 Responses

    1. It should work just fine with any attribute that doesn’t directly alter the property visualization itself.
      So things such as tool tip, space and header will behave as expected.
      Unfortunately I haven’t found a way to automatically combine custom PD’s.

  1. This sort of thing would be extremely useful for my project. However, I am writing it entirely in JS and would need to convert these scripts into JS for them to work with my scripts.

    Is that even possible? I am having a hard time trying to find the JS equivalent lines online. Would you be able to point me towards the Unity Script Reference if it exists?

    1. Well, I’ve been able to successfully convert the scripts into JS, but the ConditionalHidePropertyDrawer is having a hard time getting the attribute from the ConditionalHideAttribute script, giving me a null reference exception error when I have the object I wish to use this attribute on selected.

      Any words of wisdom? I’ve got the script in the Editor folder and even tried directly grabbing the attribute through Resources.Load via its script.

      1. Hi Max.

        I personally have no experience with JS inside of unity but I do now that instead of [ConditionalHide] (c#) you need to use @ConditionalHide when using attributes in JS (https://docs.unity3d.com/Manual/Attributes.html)

        Technically, as far as I know, you could even keep the custom propertyattribute in c# within the project and still use it inside of your JS scripts with the @ symbol. But I could be wrong on this one.

        I hope this was the the missing element to get it to work 🙂

        1. Thanks for your response. While I wasn’t able to get the converted JS scripts to function correctly I did end up realising that I could just use the C# scripts.

          I haven’t had much experience with having the two languages work together in Unity so I just assumed it was safer to convert it. I’ve grabbed the attribute with the JS syntax and it all works as it should.

          Thanks, Brechtos.

          1. Hey!

            How did you manage to get it to work from a javascript?

            The thing works just fine from a CS script with “[ConditionalHide(“test”)]”

            But it doesn’t work from unityscript files. I use it like tihs:

            @ConditionalHide(“test”)

            But I get an Unity error: “No attribute with the name ‘ConditionalHide’ or ‘ConditionalHideAttribute’ was found (attribute names are case insensitive). Did you mean ‘UnityEditor.CanEditMultipleObjects’?”

          2. Hey Beldarak,

            As far as I know, if it works in a c# script within your project it should also work for a javascript script.
            @ConditionalHide (“PropertyName”)

            It seemed to work for Max.
            Unfortunatly I have no experience with using Javascript in unity. I’m not sure if the space after the @attribute is important or not but all the unity samples on the site do seem to have that. Let me know if that solves your issue 🙂

  2. I found this very useful, thanks.

    For my own purposes I have extended it to test against floats/ints/and array size. It defaults to test these as > 0 but I also added TestValue public fields which can be specified to test against instead.

    in ConditionalHideAttribute:

    public bool TestValueBool = true;
    public int TestValueInt = 0;
    public float TestValueFloat = 0;

    and then in the test in the PropertyDrawer:

    if (sourcePropertyValue.isArray)
    return sourcePropertyValue.arraySize > condHAtt.TestValueInt;

    switch (sourcePropertyValue.propertyType)
    {
    case SerializedPropertyType.Integer:
    return sourcePropertyValue.intValue > condHAtt.TestValueInt;

    case SerializedPropertyType.Float:
    return sourcePropertyValue.floatValue > condHAtt.TestValueFloat;

    case SerializedPropertyType.Boolean:
    return sourcePropertyValue.boolValue == condHAtt.TestValueBool;


    }

    I cannot find a way to make it hide arrays properly, OnGUI appears to never receive the actual array property only the individual elements… stupid unity.

    1. Unfortunately there is indeed no clean way to hide arrays. Well… it will hide everything except the index count 🙁 .
      So far I haven’t found a way to work around this and I’m not sure if there is. It does hide custom data classes though, so an array wrapped in a custom serializable class will hide as part of the class. But that’s a bit to hacky and overkill for my taste 🙂 .

      Nice to see you were able to extend it to fit your needs. I personally also have a variation that allows you to hide based on enums etc. But for those I usually just create a completely new attribute to avoid adding to much functionality to the base ConditionalHideAttribute implementation and making it to overloaded/complex to use.

  3. Good news,

    After looking deep into this I managed to tweak something really generic, working for both encapsulated/non encapsulated parameters. The key reside in propertyPath. Here’s how I did:

        private bool GetConditionalHideAttributeResult(ConditionalHideAttribute condHAtt, SerializedProperty property)
        {
            bool enabled = true;
    
    ---> MODIFICATION START
    		string pathProp = property.propertyPath;
    		string conditionPath = pathProp.Replace(property.name, condHAtt.ConditionalSourceField);
    
    		SerializedProperty sourcePropertyValue = property.serializedObject.FindProperty(conditionPath);
    -----> MODIFICATION END
    
    		//SerializedProperty sourcePropertyValue = property.serializedObject.FindProperty(condHAtt.ConditionalSourceField);
            if (sourcePropertyValue != null)
            {
                enabled = CheckPropertyType(sourcePropertyValue);               
            }
            else
            {
                Debug.LogWarning("Attempting to use a ConditionalHideAttribute but no matching SourcePropertyValue found in object: " + condHAtt.ConditionalSourceField);
            } 
    

    Thing is serializedObject seem to always refer to the root were the variable exist, therefore being problematic when using subclasses.
    Here’s the basic subclass I was trying to use:

    public class PotatoScriptTest : MonoBehaviour
    {
    	[System.Serializable]
    	public class rigTest
    	{
    		public bool enable;
    
    		[ConditionalHideAttribute("enable", true)]
    		public string name = "Test";
    		[ConditionalHideAttribute("enable", true)]
    		public float speed = 2.0f;
    	};
    
    	public rigTest blabla;
    
    	public rigTest[] blablaArray = new rigTest[5];
    }
    
    1. Thx for the great addition CrashOverride !
      Updated the article and downloadable file with the fix/upgrade.
      Hadn’t really needed this myself yet but was probably only a matter of time 🙂

      1. Nevermind, I adjusted the script 🙂 But I can’t get the ConditionalSourceField2 to work…

        I got this code:

        [ConditionalHide(“NumberOfRotations”,ConditionalSourceField2 = “RotationType”,true,true)]

        RotationType is an enum, that might have something to do with it…

        1. Hi Alexander,
          Glad you found use for the attribute 🙂
          Yes you can use the script in an asset but it would be nice to keep the originally made by header and perhaps a mention+link in the credits 🙂

          As for the enum. Enums are actually just integer values so this will not work out of the box just like that.
          I do have an alternate version of this system that works with enums and allows me to hide properties based on the value of an enum.

          Below is the code for that. You could easily combine the 2 and create different variations but for our use it works just having both version.

              using UnityEngine;
          using System;
          using System.Collections;
          
          //Original version of the ConditionalEnumHideAttribute created by Brecht Lecluyse (www.brechtos.com)
          //Modified by: -
          
          [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property |
              AttributeTargets.Class | AttributeTargets.Struct, Inherited = true)]
          public class ConditionalEnumHideAttribute : PropertyAttribute
          {
              //The name of the bool field that will be in control
              public string ConditionalSourceField = "";
          
              public int EnumValue1 = 0;
              public int EnumValue2 = 0;
          
              public bool HideInInspector = false;
              public bool Inverse = false;
          
              public ConditionalEnumHideAttribute(string conditionalSourceField, int enumValue1)
              {
                  this.ConditionalSourceField = conditionalSourceField;
                  this.EnumValue1 = enumValue1;
                  this.EnumValue2 = enumValue1;
              }
          
              public ConditionalEnumHideAttribute(string conditionalSourceField, int enumValue1, int enumValue2)
              {
                  this.ConditionalSourceField = conditionalSourceField;
                  this.EnumValue1 = enumValue1;
                  this.EnumValue2 = enumValue2;
              }
          }
          
          using UnityEngine;
          using UnityEditor;
          
          //Original version of the ConditionalEnumHideAttribute created by Brecht Lecluyse (www.brechtos.com)
          //Modified by: -
           
          [CustomPropertyDrawer(typeof(ConditionalEnumHideAttribute))]
          public class ConditionalEnumHidePropertyDrawer : PropertyDrawer
          {
              public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
              {
                  ConditionalEnumHideAttribute condHAtt = (ConditionalEnumHideAttribute)attribute;
                  int enumValue = GetConditionalHideAttributeResult(condHAtt, property);
           
                  bool wasEnabled = GUI.enabled;
                  GUI.enabled = ((condHAtt.EnumValue1 == enumValue) || (condHAtt.EnumValue2 == enumValue));
                  if (!condHAtt.HideInInspector || (condHAtt.EnumValue1 == enumValue) || (condHAtt.EnumValue2 == enumValue))
                  {
                      EditorGUI.PropertyField(position, property, label, true);
                  }
           
                  GUI.enabled = wasEnabled;
              }
           
              public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
              {
                  ConditionalEnumHideAttribute condHAtt = (ConditionalEnumHideAttribute)attribute;
                  int enumValue = GetConditionalHideAttributeResult(condHAtt, property);
          
                  if (!condHAtt.HideInInspector || (condHAtt.EnumValue1 == enumValue) || (condHAtt.EnumValue2 == enumValue))
                  {
                      return EditorGUI.GetPropertyHeight(property, label);
                  }
                  else
                  {
                      return -EditorGUIUtility.standardVerticalSpacing;
                  }
              }
          
              private int GetConditionalHideAttributeResult(ConditionalEnumHideAttribute condHAtt, SerializedProperty property)
              {
                  int enumValue = 0;
          
                  SerializedProperty sourcePropertyValue = null;
                  //Get the full relative property path of the sourcefield so we can have nested hiding
                  if (!property.isArray)
                  {
                      string propertyPath = property.propertyPath; //returns the property path of the property we want to apply the attribute to
                      string conditionPath = propertyPath.Replace(property.name, condHAtt.ConditionalSourceField); //changes the path to the conditionalsource property path
                      sourcePropertyValue = property.serializedObject.FindProperty(conditionPath);
          
                      //if the find failed->fall back to the old system
                      if (sourcePropertyValue == null)
                      {
                          //original implementation (doens't work with nested serializedObjects)
                          sourcePropertyValue = property.serializedObject.FindProperty(condHAtt.ConditionalSourceField);
                      }
                  }
                  else
                  {
                      //original implementation (doens't work with nested serializedObjects)
                      sourcePropertyValue = property.serializedObject.FindProperty(condHAtt.ConditionalSourceField);
                  }
          
          
                  if (sourcePropertyValue != null)
                  {
                      enumValue = sourcePropertyValue.enumValueIndex;
                  }
                  else
                  {
                      //Debug.LogWarning("Attempting to use a ConditionalHideAttribute but no matching SourcePropertyValue found in object: " + condHAtt.ConditionalSourceField);
                  }
          
                  return enumValue;
              }
          
          }
          
          1. Alright, that script is pretty similar to what I did 🙂

            Thanks!

            For the credits: your name will be clearly visible and I’ll put a link to your website in the asset description

            Have a wonderful day!

            Alex

          2. Hey, me again.

            How can I make multiple attributes work alongside each other?

            for example:

            [ConditionalHide(“DrawGizmo”,true)]
            [Range(0.0F, 1.0F)]
            public float GizmoColorAlpha = 0.25F; //Opacity.

            Thanks!

          3. As far as I know this is currently not possible with PropertyDrawers in Unity . I wish it was.
            You can however stack decorator attributes as they don’t change the visualization of the data.

  4. I wanted to be able to add some more complex logic so I made a quick extension with reflection that let’s you check properties (that unity can’t serialize).

    I modified the drawer like so

    if (sourcePropertyValue != null)
            {
                enabled = CheckPropertyType(sourcePropertyValue);               
            }
            else
            {
                var propertyInfo = property.serializedObject.targetObject.GetType().GetProperty(conditionPath,
                    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
    
                if (propertyInfo != null)
                {
                    var value = propertyInfo.GetValue(property.serializedObject.targetObject);
                    enabled = CheckPropertyType(value);
                }
            }
    
    ...
    
        private static bool CheckPropertyType(object val)
        {
            if (val is bool)
            {
                return (bool) val;
            }
            return true;
        }
    

    Then you can make properties with logic in them to conditionally show

            [SerializeField] private Space _space = Space.Screen;
            private bool IsScreenSpace => _space == Space.Screen;
    
            [ConditionalHide("IsScreenSpace", true)]
            [SerializeField]
            private Position _position = Position.Bottom;
    

    This is just a simple enum example, but you can do any combination of logic.

Leave a Reply