如何在Unity检视面板中使用bool类型来启用/禁用列表?

10
我有一个名为“Level”的ScriptableObject脚本,里面有一个游戏物体列表和一个布尔变量“introduceNewEnemies”。
我想做的是:当布尔变量打开时,我想启用该游戏物体列表。当其关闭时,使用Unity自定义检视器方法或属性绘制器将其隐藏/灰化(我们无法向其中添加元素)。这难道很难实现吗?

你说的“disable”是什么意思? - John Wu
像隐藏和取消隐藏一样 - user11746000
你的意思是隐藏列表中包含的GameObjects吗?还是以某种方式隐藏列表本身(不确定这是什么意思)? - John Wu
不是元素,我指的是整个列表被隐藏或变灰,这里需要注意的重要事项是当布尔变量设置为false时,我们不能向其中添加任何元素。 - user11746000
你的列表是什么数据类型,它如何被显示出来? - John Wu
4个回答

27

最佳方法是使用自定义属性。我将从展示最终结果开始:

  • 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

如何完成?

  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();
            }
    
        }
    }
    

下一步操作

  1. 实现反向条件处理程序,即如果条件为假,则编辑以启用字段,反之亦然。
  2. 目前对于列表和数组,unit将禁用/启用列表的元素,但保持列表计数字段启用,请尝试实现解决此问题的解决方案。

非常感谢。我会尝试处理第一个问题,但第二个问题是我真的无法解决的,这就是为什么我在这里提问的原因。我尝试过这个解决方案:http://www.brechtos.com/hiding-or-disabling-inspector-properties-using-propertydrawers-within-unity-5/,但它没有起作用。请帮助我解决第二个问题。 - user11746000

1

如果不编写自定义编辑器窗口,无法将整个列表变灰。在Unity源代码中,您可以看到属性从未应用于数组。

PropertyHandler.cs文件中的HandleDrawnType方法中,从第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;

用于显示列表的内部类是UnityEditorInternal.ReorderableList本文展示了如何自定义它。我们可以使用它来强制标记有我们自定义属性的字段变灰。以下是我为任何Unity对象编写的尝试:

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();
    }
}

很遗憾,我无法使用ChoopTwisk在他们的答案中编写的属性。我没有找到一种方法来检查编辑器类内条件的结果。在我的示例中,任何标记为[NotEditable]的字段都将永久变灰。如果您想使用它,下面是其代码。我在这里找到了它。

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();
    }
}

-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;
            }
        }
}

-2
隐藏变量,请看这里
但是如果你想将字段变灰,这里就可以了。

谢谢您的回答,但是这个方法适用于变量字段,我知道如何操作,但我想根据控制布尔变量将整个列表变灰。 - user11746000

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接