Как сделать выпадающее меню инспектора Unity в виде перечисления из массива строк с C#? - PullRequest
0 голосов
/ 26 марта 2020

Я делаю сценарий 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, поскольку они жестко закодированы как «CharacterA» и «CharacterB» в DialogueElement class.

Для тех, кто не знаком с Unity, это программа для создания игр. Unity позволяет пользователям создавать физические файлы (известные как объекты сценариев), которые действуют как контейнеры для объектов классов. Переменные publi c объекта сценария могут быть определены через визуальный интерфейс, называемый «инспектор», как показано ниже:
Unity inspector

Я хочу использовать перечисление для указания того, какие символы обозначают строку диалога, потому что использование перечисления создает хорошее раскрывающееся меню в инспекторе, где пользователь может легко выбрать символ без необходимости вручную вводить имя символа для каждой строки диалог.

Как разрешить пользователю определять элементы перечисления Characters? В этом случае я пытался использовать переменную массива строк, где игрок может ввести имя из всех возможных символов, а затем использовать этот массив для определения перечисления.

Я не знаю, возможно ли решение проблемы таким способом. Я открыт для ЛЮБЫХ идей, которые позволят пользователю указать список имен, которые затем могут быть использованы для создания раскрывающегося меню в инспекторе, где пользователь выбирает одно из имен, как показано в Изображение выше.

В решении не требуется специально объявлять новое перечисление из массива строк. Я просто хочу найти способ сделать эту работу. Одно из решений, о котором я подумал, - написать отдельный скрипт, который бы редактировал текст скрипта C#, который содержит перечисление символов. Я думаю, что технически это сработает, поскольку Unity автоматически перекомпилирует сценарии каждый раз, когда обнаруживает, что они были изменены, и обновляет объекты сценариев в инспекторе, но я надеялся найти более понятный способ.

Ссылка на репозиторий для справки:
https://github.com/guitarjorge24/DialogueTool

1 Ответ

2 голосов
/ 26 марта 2020

Вы не можете изменить само перечисление, так как оно должно быть скомпилировано (ну, это не совсем невозможно, но я бы не рекомендовал go способов, таких как активное изменение скрипта и принудительная перекомпиляция)


Не видя остальные нужные вам типы, это немного сложно, но то, что вы хотите, лучше всего делать в скрипте пользовательского редактора, используя EditorGUILayout.Popup. Как я уже сказал, я не знаю ваших точных потребностей и типа Characters или того, как именно вы на них ссылаетесь, поэтому сейчас я предполагаю, что вы ссылаетесь на DialogueElement на определенный символ через его индекс в списке Dialogue.CharactersList. Это в основном работает как 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

И вот как это будет выглядеть сейчас

enter image description here

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...