10

I have a ScriptableObject script called Level, inside the level script I have a List of GameObjects and a bool variable called introduceNewEnemies.

What i'm trying to do is that: I want to enable that List of gameobjects when the bool variable is on and Hide/Gray out (we can't add elements to it) it when its off using Unity custom inspector methods or Property drawers. Is it that hard to do?

4 Answers4

25

Best approach is a custom attribute. I'll start by showing the end result:

  • Using a field to hide/show another field:

    public bool showHideList = false; 
    [ShowIf(ActionOnConditionFail.DontDraw, ConditionOperator.And, nameof(showHideList))]
    public string aField = "item 1";
    

    Image from Gyazo

  • Using a field to enable/disable another field:

    public bool enableDisableList = false;
    
    [ShowIf(ActionOnConditionFail.JustDisable, ConditionOperator.And, 
    nameof(enableDisableList))]
    public string anotherField = "item 2";
    

    Image from Gyazo

  • Using a method to get a condition value:

    [ShowIf(ActionOnConditionFail.JustDisable, ConditionOperator.And,nameof(CalculateIsEnabled))]
    public string yetAnotherField = "one more";    public 
    bool CalculateIsEnabled()    
    {
        return true;    
    }
    

    Image from Gyazo

  • Using multiple conditions on the same field:

    public bool condition1;    
    public bool condition2;    
    [ShowIf(ActionOnConditionFail.JustDisable, ConditionOperator.And, nameof(condition1), 
    nameof(condition2))]    
    public string oneLastField= "last field";
    

    Image from Gyazo


How was it done ?

  1. Define options for allowing mutiple conditions at once:

    public enum ConditionOperator
    {
        // A field is visible/enabled only if all conditions are true.
        And,
        // A field is visible/enabled if at least ONE condition is true.
        Or,
    }
    
  2. Define how the field is drawn if the condition fails:

    public enum ActionOnConditionFail
    {
        // If condition(s) are false, don't draw the field at all.
        DontDraw,
        // If condition(s) are false, just set the field as disabled.
        JustDisable,
    }
    
  3. Now create a custom attribute class, to hold data about the condition:

    using System;
    using UnityEngine;
    [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
    public class ShowIfAttribute : PropertyAttribute
    {
        public ActionOnConditionFail Action {get;private set;}
        public ConditionOperator Operator {get;private set;}
        public string[] Conditions {get;private set;}
    
         public ShowIfAttribute(ActionOnConditionFail action, ConditionOperator conditionOperator, params string[] conditions)
        {
            Action  = action;
            Operator = conditionOperator;
            Conditions = conditions;
        }
    }
    
  4. The meaty part where we tell unity how to handle fields with ShowIfAttribute, this Drawer script needs to be under any 'Editor' folder:

    using System.Reflection;
    using UnityEditor;
    using System.Collections.Generic;
    using System;
    using System.Linq;
    using UnityEngine;
    
    [CustomPropertyDrawer(typeof(ShowIfAttribute), true)]
    public class ShowIfAttributeDrawer : PropertyDrawer
    {
    
        #region Reflection helpers.
        private static MethodInfo GetMethod(object target, string methodName)
        {
            return GetAllMethods(target, m => m.Name.Equals(methodName, 
                      StringComparison.InvariantCulture)).FirstOrDefault();
        }
    
        private static FieldInfo GetField(object target, string fieldName)
        {
            return GetAllFields(target, f => f.Name.Equals(fieldName, 
                  StringComparison.InvariantCulture)).FirstOrDefault();
        }
        private static IEnumerable<FieldInfo> GetAllFields(object target, Func<FieldInfo, 
                bool> predicate)
        {
            List<Type> types = new List<Type>()
                {
                    target.GetType()
                };
    
            while (types.Last().BaseType != null)
            {
                types.Add(types.Last().BaseType);
            }
    
            for (int i = types.Count - 1; i >= 0; i--)
            {
                IEnumerable<FieldInfo> fieldInfos = types[i]
                    .GetFields(BindingFlags.Instance | BindingFlags.Static | 
       BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly)
                    .Where(predicate);
    
                foreach (var fieldInfo in fieldInfos)
                {
                    yield return fieldInfo;
                }
            }
        }
        private static IEnumerable<MethodInfo> GetAllMethods(object target, 
      Func<MethodInfo, bool> predicate)
        {
            IEnumerable<MethodInfo> methodInfos = target.GetType()
                .GetMethods(BindingFlags.Instance | BindingFlags.Static | 
      BindingFlags.NonPublic | BindingFlags.Public)
                .Where(predicate);
    
            return methodInfos;
        }
        #endregion
    
        private bool MeetsConditions(SerializedProperty property)
        {
            var showIfAttribute = this.attribute as ShowIfAttribute;
            var target = property.serializedObject.targetObject;
            List<bool> conditionValues = new List<bool>();
    
            foreach (var condition in showIfAttribute.Conditions)
            {
                FieldInfo conditionField = GetField(target, condition);
                if (conditionField != null &&
                    conditionField.FieldType == typeof(bool))
                {
                    conditionValues.Add((bool)conditionField.GetValue(target));
                }
    
                MethodInfo conditionMethod = GetMethod(target, condition);
                if (conditionMethod != null &&
                    conditionMethod.ReturnType == typeof(bool) &&
                    conditionMethod.GetParameters().Length == 0)
                {
                    conditionValues.Add((bool)conditionMethod.Invoke(target, null));
                }
            }
    
            if (conditionValues.Count > 0)
            {
                bool met;
                if (showIfAttribute.Operator == ConditionOperator.And)
                {
                    met = true;
                    foreach (var value in conditionValues)
                    {
                        met = met && value;
                    }
                }
                else
                {
                    met = false;
                    foreach (var value in conditionValues)
                    {
                        met = met || value;
                    }
                }
                return met;
            }
            else
            {
                Debug.LogError("Invalid boolean condition fields or methods used!");
                return true;
            }
        }
        public override float GetPropertyHeight(SerializedProperty property, GUIContent 
                     label)
        {
            // Calcluate the property height, if we don't meet the condition and the draw 
        mode is DontDraw, then height will be 0.
            bool meetsCondition = MeetsConditions(property);
            var showIfAttribute = this.attribute as ShowIfAttribute;
    
            if (!meetsCondition && showIfAttribute.Action == 
                                           ActionOnConditionFail.DontDraw)
                return 0;
            return base.GetPropertyHeight(property, label);
        }
    
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent 
               label)
        {
            bool meetsCondition = MeetsConditions(property);
            // Early out, if conditions met, draw and go.
            if (meetsCondition)
            {
                EditorGUI.PropertyField(position, property, label, true);
                return; 
            }
    
            var showIfAttribute = this.attribute as ShowIfAttribute;
            if(showIfAttribute.Action == ActionOnConditionFail.DontDraw)
            {
                return;
            }
            else if (showIfAttribute.Action == ActionOnConditionFail.JustDisable)
            {
                EditorGUI.BeginDisabledGroup(true);
                EditorGUI.PropertyField(position, property, label, true);
                EditorGUI.EndDisabledGroup();
            }
    
        }
    }
    

Next steps

  1. Implement a reverse condition handler, that is, edit it to enable a field if the conditions are false, and vice versa.
  2. Currently for lists and arrays, unit will disable/enable the elements of the list, but keep the list count field enabled, try to implement a solution to handle this case.
ChoopTwisk
  • 1,296
  • 7
  • 13
  • Thank you very much. I'll try to handle the first one but the second is something that I really can't do and thats why I ask a question here, I tried to do this solution: http://www.brechtos.com/hiding-or-disabling-inspector-properties-using-propertydrawers-within-unity-5/ but it didn't work. Please help me fix the second case. –  Oct 18 '19 at 13:51
1

It is not possible to gray out the entire list without writing a custom editor window. In the Unity source code, you can see that attributes are never applied to arrays.

In PropertyHandler.cs within the method HandleDrawnType, beginning on line 102:

// Use PropertyDrawer on array elements, not on array itself.
// If there's a PropertyAttribute on an array, we want to apply it to the individual array elements instead.
// This is the only convenient way we can let the user apply PropertyDrawer attributes to elements inside an array.
if (propertyType != null && propertyType.IsArrayOrList())
    return;

The class used internally to display lists is UnityEditorInternal.ReorderableList. This article shows how it can be customized. We can use this to force fields marked with our custom attribute to be grayed out. Here is my attempt, written for any Unity object:

using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using System;
using System.Reflection;
using System.Collections.Generic;

[CustomEditor(typeof(UnityEngine.Object), true), CanEditMultipleObjects]
public class FullyDisabledListEditor : Editor
{
    Dictionary<string, ReorderableList> _disabledLists;

    private void OnEnable() 
    {
        if(_disabledLists == null) _disabledLists = new();

        var fields = serializedObject.targetObject.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
        foreach(FieldInfo field in fields)
        {
            if(!IsArrayOrList(field.FieldType)) continue;

            IEnumerator<CustomAttributeData> attributes = field.CustomAttributes.GetEnumerator();
            while (attributes.MoveNext())
            {
                if(attributes.Current.AttributeType != typeof(NotEditableAttribute)) continue;

                CreateDisabledList(field.Name);
            }
        }
    }

    private bool IsArrayOrList(Type listType)
    {
        if (listType.IsArray)
            return true;
        else if (listType.IsGenericType && listType.GetGenericTypeDefinition() == typeof(List<>))
            return true;
        return false;
    }

    private void CreateDisabledList(string fieldName)
    {
        var listProperty = serializedObject.FindProperty(fieldName); 

        var list = new ReorderableList(
                serializedObject, 
                listProperty,
                false,  //this setting allows you to rearrange the items
                true,   //this setting displays a header label
                false,  //this setting removes the 'add elements' button
                false   //this setting removes the 'remove elements' button
                );

        list.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
        {
            var property = list.serializedProperty.GetArrayElementAtIndex(index);

            rect.y += 2; //for some reason fields are vertically off center by default
            EditorGUI.PropertyField(rect, property);
        };
        
        list.drawHeaderCallback = (Rect rect) => {
            EditorGUI.LabelField(rect, listProperty.displayName);
        };

        _disabledLists.Add(fieldName, list);
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();

        SerializedProperty property = serializedObject.GetIterator();
        while (property.NextVisible(true))
            HandlePropertyDisplay(property);

        serializedObject.ApplyModifiedProperties();
    }

    private void HandlePropertyDisplay(SerializedProperty property)
    {
        //skip redrawing child properties
        if(property.propertyPath.Contains('.', StringComparison.Ordinal)) return;

        //gray out the script, as it normally is
        if(property.propertyPath.Equals("m_Script", StringComparison.Ordinal))
        {
            ShowDisabledProperty(property);
            return;
        }

        //gray out the arrays we marked
        if(_disabledLists.ContainsKey(property.name))
        {
            _disabledLists[property.name].DoLayoutList();
            return;
        }

        //everything else is shown normally (non-array fields will be grayed out by the attribute drawer)
        EditorGUILayout.PropertyField(property);
    }

    private void ShowDisabledProperty(SerializedProperty property)
    {
        EditorGUI.BeginDisabledGroup(true);
        EditorGUILayout.PropertyField(property, true);
        EditorGUI.EndDisabledGroup();
    }
}

Unfortunately, I couldn't use the attribute written by ChoopTwisk in their answer. I didn't figure out a way to check the result of the conditions from within the editor class. In my example, any field marked with [NotEditable] will be permanently grayed out. If you want to use that, the code for it is below. I found it here.

using UnityEditor;
using UnityEngine;

public class NotEditableAttribute : PropertyAttribute { }

[CustomPropertyDrawer(typeof(NotEditableAttribute))]
public sealed class NotEditableDrawer : PropertyDrawer
{
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return EditorGUI.GetPropertyHeight(property, label, true);
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginDisabledGroup(true);
        EditorGUI.PropertyField(position, property, label, true);
        EditorGUI.EndDisabledGroup();
    }
}
Bacon Nugget
  • 107
  • 11
-1
public class Test : MonoBehaviour
{
    [SerializeField]
    bool openFlag = false;
    bool prevOpenFlag = false;
    void Update()
    {
        if(prevOpenFlag != openFlag)
        {
            if (openFlag == true)
            {
                Open();
                prevOpenFlag = openFlag;
            }else
            {
                Close();
                prevOpenFlag = openFlag;
            }
        }
}
TimChang
  • 2,249
  • 13
  • 25
-2

To hide a variable, take a look here.

But if you want to gray out a field, here you go.

Malphegal
  • 270
  • 1
  • 4
  • 10
  • Thank for your answer but you this works for variable field and I know how to do it but I want to gray out an entire list based on a control bool variable. –  Oct 18 '19 at 01:19