unity3d 如何使用C#从字符串数组创建一个类似枚举的Unity检查器下拉菜单?

iaqfqrcu  于 2022-11-30  发布在  C#
关注(0)|答案(4)|浏览(395)

我正在制作一个Unity C#脚本,该脚本旨在被其他人用作角色对话工具,以写出多个游戏角色之间的对话。
我有一个DialogueElement类,然后创建一个DialogueElement对象列表,每个对象代表一行对话。
第一个
我希望用户能够通过只与Unity检查器交互来使用对话框工具(因此没有编辑代码)。这种设置的问题是,对话框工具的用户无法为Characters枚举中的字符指定他们自己的自定义名称(如Felix或Wendy),因为它们在DialogueElement类中被硬编码为“CharacterA”和“CharacterB”。
对于那些不熟悉Unity的人来说,它是一个游戏创建程序。Unity允许用户创建物理文件(称为可脚本化对象),作为类对象的容器。可脚本化对象的公共变量可以通过一个称为“检查器”的可视界面来定义,如下所示:

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

**如何允许用户定义Characters枚举的元素?**在本例中,我尝试使用字符串数组变量,播放器可以在其中键入所有可能字符的名称,然后使用该数组定义枚举。

我不知道这样解决问题是否可行。我对任何想法都持开放态度,允许用户指定一个名称列表,然后在检查器中创建一个下拉菜单,用户可以在其中选择一个名称,如上图所示。
这个解决方案不需要从字符串数组中特别声明一个新的枚举。我只是想找到一种方法来实现这个功能。我想到的一个解决方案是编写一个单独的脚本,用来编辑包含字符枚举的C#脚本的文本。我认为这在技术上是可行的,因为Unity会在每次检测到脚本被更改时自动重新编译脚本,并更新检查器中的可脚本化对象,但我希望能找到更干净的方法。
链接到存储库以供参考:
https://github.com/guitarjorge24/DialogueTool

xwmevbvl

xwmevbvl1#

你不能改变枚举本身,因为它需要被编译(当然,这不是完全不可能的,但是我不建议你主动改变脚本并强制重新编译)
没有看到你所需要的其他类型是有点困难的,但是你最好在一个自定义的编辑器脚本中使用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

这就是它现在的样子

kiz8lqtg

kiz8lqtg2#

对于那些谁需要枚举像字符串下拉,你可以使用代码从我的github repo.下载这个文件夹,并添加到您的项目. https://github.com/datsfain/EditorCools/tree/main/Assets/Tools/DropdownStringAttribute
将以下代码行添加到任何可序列化的类中:

[Dropdown(nameof(MethodThatReturnsStringArray))]
public string options2;
sf6xfgos

sf6xfgos3#

另一个选项是使用检查器增强资源,如Odin Inspector或NaughtyAttributes。
如果你有这样一个成员:

public string[] CharactersList;

对于奥丁,你会这样写:

[ValueDropdown("CharactersList")]

在NaughtyAttributes中,您可以写入:

[Dropdown("CharactersList")]

这些解决方案类似于datsfain的建议。
虽然Odin不是免费的,但它有很多额外的功能。https://odininspector.com/attributes/value-dropdown-attribute
NaughtyAttributes是免费的,但有点旧,更基本。https://dbrizov.github.io/na-docs/attributes/drawer_attributes/dropdown.html

4xrmg8kj

4xrmg8kj4#

我要写另一个答案...因为双重答案更好!
正如derHugo在他的回答中所说的,可以使用枚举来完成,但是它会强制重新编译。嗯,有时候也许你真的需要枚举(在某些情况下,它们比字符串快得多),并且你愿意接受重新编译的代价。
这里有一个我写的小工具类,用于生成枚举并将其保存到文件中。
创建一个成员变量,其中包含设计者可以编辑的字符串列表。您可能希望在UI上放置一个名为“GenerateEnums”的按钮或类似的按钮,还可能放置一个保存目录的字符串,它将调用save函数并将枚举定义写入文件。有代码强制重新编译,因此每当设计者按下该按钮时,他们需要等待几秒钟。此外,这里有一个先有鸡还是先有蛋的问题--在定义至少生成一次之前,您不能引用该类型。(就像“public enum CharacterType { dummy }”).在设计者编辑字符串列表之后,按下生成按钮,并等待几秒钟,他们将能够在使用该枚举类型(本例中为CharacterType)的任何字段中看到更新后的选择。

// this has a bunch of functions for generating enums in the editor

using System.Collections.Generic;

public static class EnumUtils
{
private static readonly HashSet<string> m_keywords = new HashSet<string> {
    "abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked",
    "class", "const", "continue", "decimal", "default", "delegate", "do", "double", "else",
    "enum", "event", "explicit", "extern", "false", "finally", "fixed", "float", "for",
    "foreach", "goto", "if", "implicit", "in", "int", "interface", "internal", "is", "lock",
    "long", "namespace", "new", "null", "object", "operator", "out", "override", "params",
    "private", "protected", "public", "readonly", "ref", "return", "sbyte", "sealed",
    "short", "sizeof", "stackalloc", "static", "string", "struct", "switch", "this", "throw",
    "true", "try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using",
    "virtual", "void", "volatile", "while"
};

// This function will return a string containing an enum declaration with the specified parameters.
// name --> the name of the enum to create
// values -> the enum values
// primitive --> byte, int, uint, short, int64, etc (empty string means no type specifier)
// makeClassSize --> if this is true, an extra line will be added that makes a static class to hold the size.
// example:
//    print(MakeEnumDeclaration("MyType", { Option1, Option2, Option3 }, "byte", true));
//    output -->  public enum MyType : byte { Option1, Option2, Option3 }
//                public static class MyTypeSize { public const byte Size = 3; }
public static string MakeEnumDeclaration(string name, List<string> values, string primitive, bool makeSizeClass)
{
    string prim = primitive.Length <= 0 ? "" : " : " + primitive;
    string declaration = "public enum " + name + prim + " { ";
    int countMinusOne = values.Count - 1;
    for (int i = 0; i < values.Count; i++) {
        declaration += MakeStringEnumCompatible(values[i]);
        if (i < countMinusOne) { declaration += ", "; }
    }
    declaration += " }\n";
    if (makeSizeClass) {
        declaration += $"public static class {name}Size {{ public const {primitive} Size = {values.Count}; }}\n";
    }
    return declaration;
}

public static void WriteDeclarationToFile(string fileName, string declaration, bool reImport = false, string filePath = "Assets/Scripts/Generated/")
{
    // ensure that the output directory exists
    System.IO.Directory.CreateDirectory(filePath);
    // write the file
    System.IO.File.WriteAllText(filePath + fileName, "// This file was auto-generated\n\n" + declaration);
    #if UNITY_EDITOR
        if (reImport) { UnityEditor.AssetDatabase.ImportAsset(filePath); }
    #endif
}

public static void WriteDeclarationsToFile(string fileName, List<string> declarations, bool reImport = false, string filePath = "Assets/Scripts/Generated/")
{
    string text = "";
    for (int i = 0; i < declarations.Count; i++) { text += declarations[i]; }
    WriteDeclarationToFile(fileName, text, reImport, filePath);
}

// given a string, attempts to make the string compatible with an enum
// if there are any spaces, it will attempt to make the string camel-case
public static string MakeStringEnumCompatible(string text)
{
    if (text.Length <= 0) { return "INVALID_ENUM_NAME"; }
    string ret = "";

    // first char must be a letter or an underscore, so ignore anything that is not
    if (char.IsLetter(text[0]) || (text[0] == '_')) { ret += text[0]; }

    // ignore anything that's not a digit or underscore
    bool enableCapitalizeNextLetter = false;
    for (int i = 1; i < text.Length; ++i) {
        if (char.IsLetterOrDigit(text[i]) || (text[i] == '_')) {
            if (enableCapitalizeNextLetter) {
                ret += char.ToUpper(text[i]);
            } else {
                ret += text[i];
            }
            enableCapitalizeNextLetter = false;
        } else if (char.IsWhiteSpace(text[i])) {
            enableCapitalizeNextLetter = true;
        }
    }
    if (ret.Length <= 0) { return "INVALID_ENUM_NAME"; }

    // all the keywords are lowercase, so if we just change the first letter to uppercase,
    // then there will be no conflict
    if (m_keywords.Contains(ret)) { ret = char.ToUpper(ret[0]) + ret.Substring(1); }

    return ret;
}
}

注意事项:
请记住,这个方法有点脆弱,就像你在编辑器中使用枚举一样。在值的中间删除或插入枚举值可能会导致使用该枚举的任何字段使用错误的索引,因为所有索引都将移动一位。不会有编译器错误,而且你可能会在运行时得到奇怪的活动。
如果在代码中使用枚举值,删除或重命名该枚举值可能会导致代码停止编译(这可能是件好事-使用字符串时没有编译器错误,并且在运行时您会得到一个无声的令人困惑的惊喜)。

相关问题