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

Jor*_*que 0 c# enums unity-game-engine unity-editor

我正在制作一个 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
}
Run Code Online (Sandbox Code Playgroud)
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
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

不知道这样解决问题是否可行。我愿意接受任何允许用户指定名称列表的想法,然后可以使用这些名称在检查器中创建下拉菜单,用户可以在其中选择一个名称,如上图所示。

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

链接到存储库以供参考:https :
//github.com/guitarjorge24/DialogueTool

der*_*ugo 9

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


如果没有看到您需要的其余类型,这有点困难,但是您最好在自定义编辑器脚本中使用EditorGUILayout.Popup. 如前所述,我不知道您的确切需求和类型,也不知道您Characters引用它们的确切方式,因此现在我假设您DialogueElement通过列表中的索引引用某个字符Dialogue.CharactersList。这基本上就像一个enumthen!

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

    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
Run Code Online (Sandbox Code Playgroud)

这就是它现在的样子

在此处输入图片说明