如何使用 C# 从字符串数组制作类似枚举的 Unity 检查器下拉菜单?

Posted

技术标签:

【中文标题】如何使用 C# 从字符串数组制作类似枚举的 Unity 检查器下拉菜单?【英文标题】:How to make an enum-like Unity inspector drop-down menu from a string array with C#? 【发布时间】:2020-07-06 21:52:59 【问题描述】:

我正在制作一个 Unity C# 脚本,旨在供其他人用作角色对话工具来写出多个游戏角色之间的对话。

我有一个DialogueElement 类,然后我创建了一个DialogueElement 对象列表。每个对象代表一行对话。

[System.Serializable] //needed to make ScriptableObject out of this class
public class DialogueElement

    public enum Characters CharacterA, CharacterB;
    public Characters Character; //Which characters is saying the line of dialog
    public string DialogueText; //What the character is saying

public class Dialogue : ScriptableObject

    public string[] CharactersList; //defined by the user in the Unity inspector
    public List<DialogueElement> DialogueItems; //each element represents a line of dialogue

我希望用户能够通过仅与 Unity 检查器交互来使用对话框工具(因此无需编辑代码)。这种设置的问题是对话工具的用户无法为Characters 枚举中的字符指定他们自己的自定义名称(例如 Felix 或 Wendy),因为它们在DialogueElement 班级。

对于不熟悉 Unity 的人来说,它是一个游戏创作程序。 Unity 允许用户创建充当类对象容器的物理文件(称为可编写脚本的对象)。可编写脚本对象的公共变量可以通过称为“检查器”的可视界面定义,如下所示:

我想使用枚举来指定哪些字符是说对话行的字符,因为使用枚举会在检查器中创建一个不错的下拉菜单,用户可以在其中轻松选择字符而无需手动输入名称每行对话的角色。

如何允许用户定义 Characters 枚举的元素? 在这种情况下,我尝试使用字符串数组变量,玩家可以在其中键入所有可能的名称字符,然后使用该数组来定义枚举。

我不知道这样解决问题是否可行。我对 ANY 的想法持开放态度,这些想法将允许用户指定一个名称列表,然后可以使用这些名称在检查器中创建一个下拉菜单,用户可以在其中选择一个名称,如上图。

该解决方案不需要专门从字符串数组中声明一个新的枚举。我只是想找到一种方法来完成这项工作。我想到的一种解决方案是编写一个单独的脚本来编辑包含 Character 枚举的 C# 脚本的文本。我认为这在技术上是可行的,因为 Unity 每次检测到脚本被更改并更新检查器中的可编写脚本的对象时都会自动重新编译脚本,但我希望找到一种更简洁的方法。

参考资料库链接:https://github.com/guitarjorge24/DialogueTool

【问题讨论】:

你不能改变 enum 本身,因为它需要被编译(好吧,这不是完全不可能的,但我不建议采取主动改变脚本并强制重新compile) ;) 但是,您可以使用自定义编辑器并使用 EditorGUILayout.Popup 之类的东西,您可以使用 string 可用选项数组(包括用户定义的选项),或者因为您已经有一个简单的字符列表使用他们的索引 你能添加类型Characters吗? 【参考方案1】:

您不能更改枚举本身,因为它需要被编译(虽然这并非完全不可能,但我不建议采取主动更改脚本并强制重新编译等方式)


如果没有看到您需要的其余类型,这有点困难,但您最好使用EditorGUILayout.Popup 在自定义编辑器脚本中执行您想要的操作。如前所述,我不知道您的确切需求以及Characters 的类型或您如何准确地引用它们,所以现在我假设您通过列表Dialogue.CharactersList 中的索引将您的DialogueElement 引用到某个字符。这基本上就像enum 那样工作!

由于这些编辑器脚本可能会变得相当复杂,因此我尝试对每一步进行评论:

    using System;
    using System.Collections.Generic;
    using System.Linq;
#if UNITY_EDITOR
    using UnityEditor;
    using UnityEditorInternal;
#endif
    using UnityEngine;

    [CreateAssetMenu]
    public class Dialogue : ScriptableObject
    
        public string[] CharactersList;
        public List<DialogueElement> DialogueItems;
    

    [Serializable] //needed to make ScriptableObject out of this class
    public class DialogueElement
    
        // You would only store an index to the according character
        // Since I don't have your Characters type for now lets reference them via the Dialogue.CharactersList
        public int CharacterID;

        //public Characters Character; 

        // By using the attribute [TextArea] this creates a nice multi-line text are field
        // You could further configure it with a min and max line size if you want: [TextArea(minLines, maxLines)]
        [TextArea] public string DialogueText;
    

    // This needs to be either wrapped by #if UNITY_EDITOR
    // or placed in a folder called "Editor"
#if UNITY_EDITOR
    [CustomEditor(typeof(Dialogue))]
    public class DialogueEditor : Editor
    
        // This will be the serialized clone property of Dialogue.CharacterList
        private SerializedProperty CharactersList;

        // This will be the serialized clone property of Dialogue.DialogueItems
        private SerializedProperty DialogueItems;

        // This is a little bonus from my side!
        // These Lists are extremely more powerful then the default presentation of lists!
        // you can/have to implement completely custom behavior of how to display and edit 
        // the list elements
        private ReorderableList charactersList;
        private ReorderableList dialogItemsList;

        // Reference to the actual Dialogue instance this Inspector belongs to
        private Dialogue dialogue;

        // class field for storing available options
        private GuiContent[] availableOptions;

        // Called when the Inspector is opened / ScriptableObject is selected
        private void OnEnable()
        
            // Get the target as the type you are actually using
            dialogue = (Dialogue) target;

            // Link in serialized fields to their according SerializedProperties
            CharactersList = serializedObject.FindProperty(nameof(Dialogue.CharactersList));
            DialogueItems = serializedObject.FindProperty(nameof(Dialogue.DialogueItems));

            // Setup and configure the charactersList we will use to display the content of the CharactersList 
            // in a nicer way
            charactersList = new ReorderableList(serializedObject, CharactersList)
            
                displayAdd = true,
                displayRemove = true,
                draggable = false, // for now disable reorder feature since we later go by index!

                // As the header we simply want to see the usual display name of the CharactersList
                drawHeaderCallback = rect => EditorGUI.LabelField(rect, CharactersList.displayName),

                // How shall elements be displayed
                drawElementCallback = (rect, index, focused, active) =>
                
                    // get the current element's SerializedProperty
                    var element = CharactersList.GetArrayElementAtIndex(index);

                    // Get all characters as string[]
                    var availableIDs = dialogue.CharactersList;

                    // store the original GUI.color
                    var color = GUI.color;
                    // Tint the field in red for invalid values
                    // either because it is empty or a duplicate
                    if(string.IsNullOrWhiteSpace(element.stringValue) || availableIDs.Count(item => string.Equals(item, element.stringValue)) > 1)
                    
                        GUI.color = Color.red;
                    
                    // Draw the property which automatically will select the correct drawer -> a single line text field
                    EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width, EditorGUI.GetPropertyHeight(element)), element);

                    // reset to the default color
                    GUI.color = color;

                    // If the value is invalid draw a HelpBox to explain why it is invalid
                    if (string.IsNullOrWhiteSpace(element.stringValue))
                    
                        rect.y += EditorGUI.GetPropertyHeight(element);
                        EditorGUI.HelpBox(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), "ID may not be empty!", MessageType.Error );
                    else if (availableIDs.Count(item => string.Equals(item, element.stringValue)) > 1)
                    
                        rect.y += EditorGUI.GetPropertyHeight(element);
                        EditorGUI.HelpBox(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), "Duplicate! ID has to be unique!", MessageType.Error );
                    
                ,

                // Get the correct display height of elements in the list
                // according to their values
                // in this case e.g. dependent whether a HelpBox is displayed or not
                elementHeightCallback = index =>
                
                    var element = CharactersList.GetArrayElementAtIndex(index);
                    var availableIDs = dialogue.CharactersList;

                    var height = EditorGUI.GetPropertyHeight(element);

                    if (string.IsNullOrWhiteSpace(element.stringValue) || availableIDs.Count(item => string.Equals(item, element.stringValue)) > 1)
                    
                        height += EditorGUIUtility.singleLineHeight;
                    

                    return height;
                ,

                // Overwrite what shall be done when an element is added via the +
                // Reset all values to the defaults for new added elements
                // By default Unity would clone the values from the last or selected element otherwise
                onAddCallback = list =>
                
                    // This adds the new element but copies all values of the select or last element in the list
                    list.serializedProperty.arraySize++;

                    var newElement = list.serializedProperty.GetArrayElementAtIndex(list.serializedProperty.arraySize - 1);
                    newElement.stringValue = "";
                

            ;

            // Setup and configure the dialogItemsList we will use to display the content of the DialogueItems 
            // in a nicer way
            dialogItemsList = new ReorderableList(serializedObject, DialogueItems)
            
                displayAdd = true,
                displayRemove = true,
                draggable = true, // for the dialogue items we can allow re-ordering

                // As the header we simply want to see the usual display name of the DialogueItems
                drawHeaderCallback = rect => EditorGUI.LabelField(rect, DialogueItems.displayName),

                // How shall elements be displayed
                drawElementCallback = (rect, index, focused, active) =>
                
                    // get the current element's SerializedProperty
                    var element = DialogueItems.GetArrayElementAtIndex(index);

                    // Get the nested property fields of the DialogueElement class
                    var character = element.FindPropertyRelative(nameof(DialogueElement.CharacterID));
                    var text = element.FindPropertyRelative(nameof(DialogueElement.DialogueText));

                    var popUpHeight = EditorGUI.GetPropertyHeight(character);

                    // store the original GUI.color
                    var color = GUI.color;

                    // if the value is invalid tint the next field red
                    if(character.intValue < 0) GUI.color = Color.red;

                    // Draw the Popup so you can select from the existing character names
                    character.intValue =  EditorGUI.Popup(new Rect(rect.x, rect.y, rect.width, popUpHeight), new GUIContent(character.displayName), character.intValue,  availableOptions);

                    // reset the GUI.color
                    GUI.color = color;
                    rect.y += popUpHeight;

                    // Draw the text field
                    // since we use a PropertyField it will automatically recognize that this field is tagged [TextArea]
                    // and will choose the correct drawer accordingly
                    var textHeight = EditorGUI.GetPropertyHeight(text);
                    EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width, textHeight), text);
                ,

                // Get the correct display height of elements in the list
                // according to their values
                // in this case e.g. we add an additional line as a little spacing between elements
                elementHeightCallback = index =>
                
                    var element = DialogueItems.GetArrayElementAtIndex(index);

                    var character = element.FindPropertyRelative(nameof(DialogueElement.CharacterID));
                    var text = element.FindPropertyRelative(nameof(DialogueElement.DialogueText));

                    return EditorGUI.GetPropertyHeight(character) + EditorGUI.GetPropertyHeight(text) + EditorGUIUtility.singleLineHeight;
                ,

                // Overwrite what shall be done when an element is added via the +
                // Reset all values to the defaults for new added elements
                // By default Unity would clone the values from the last or selected element otherwise
                onAddCallback = list =>
                
                    // This adds the new element but copies all values of the select or last element in the list
                    list.serializedProperty.arraySize++;

                    var newElement = list.serializedProperty.GetArrayElementAtIndex(list.serializedProperty.arraySize - 1);
                    var character = newElement.FindPropertyRelative(nameof(DialogueElement.CharacterID));
                    var text = newElement.FindPropertyRelative(nameof(DialogueElement.DialogueText));

                    character.intValue = -1;
                    text.stringValue = "";
                
            ;

            // Get the existing character names ONCE as GuiContent[]
            // Later only update this if the charcterList was changed
            availableOptions = dialogue.CharactersList.Select(item => new GUIContent(item)).ToArray();
        

        public override void OnInspectorGUI()
        
            DrawScriptField();

            // load real target values into SerializedProperties
            serializedObject.Update();

            EditorGUI.BeginChangeCheck();
            charactersList.DoLayoutList();
            if(EditorGUI.EndChangeCheck())
            
                // Write back changed values into the real target
                serializedObject.ApplyModifiedProperties();

                // Update the existing character names as GuiContent[]
                availableOptions = dialogue.CharactersList.Select(item => new GUIContent(item)).ToArray();
            

            dialogItemsList.DoLayoutList();

            // Write back changed values into the real target
            serializedObject.ApplyModifiedProperties();
        

        private void DrawScriptField()
        
            EditorGUI.BeginDisabledGroup(true);
            EditorGUILayout.ObjectField("Script", MonoScript.FromScriptableObject((Dialogue)target), typeof(Dialogue), false);
            EditorGUI.EndDisabledGroup();

            EditorGUILayout.Space();
        
    
#endif

这就是现在的样子

【讨论】:

谢谢,太好了!但是,有一个问题是我无法通过单击减号“ - ”符号从字符列表中删除字符。你知道为什么会这样吗? 您要删除的项目是否被选中? 哦,我明白了,我认为单击文本框编辑角色名称算作选择项目,但我必须单击“元素 1”等。再次感谢!是否有您推荐的学习 Unity 编辑器脚本的资源?我想从 DialogueElement 类中添加更多属性,例如字符图片、用于选择文本颜色的 GUIStyle 属性等等。 很高兴为您提供帮助 :) 老实说,我通过 3 个月的反复试验和大量研究 API 和谷歌来学习它......哦,还有 *** ;)......不幸的是,我不知道除此之外,我可以在这里推荐的具体来源:) 哇,这真是令人印象深刻。一旦我在单个 Dialogue 可编写脚本的对象中有大约 17 个或更多对话框项,自定义编辑器代码就会出现滞后问题。就像我会完成一个句子的输入,但文本直到 4 秒后才会出现在检查器的文本框中。在修改您在此处共享的编辑器脚本之前,我已恢复到较旧的项目版本,但它仍然滞后很多。然后我恢复到使用自定义编辑器脚本之前,即使有超过 20 个对话项也没有延迟。您知道是什么原因造成的吗?

以上是关于如何使用 C# 从字符串数组制作类似枚举的 Unity 检查器下拉菜单?的主要内容,如果未能解决你的问题,请参考以下文章

☀️ 学会编程入门必备 C# 最基础知识介绍——数组字符串结构体枚举类

C#如何使用带开关的枚举

从数组中获取通用枚举器

从正则表达式制作数组

在 C# 中创建基于可枚举数组的类

如何在 C# 中制作类似 Delphi 的框架?