如何将 ReorderableList 与检查器中的列表一起使用并添加新的空项目和折叠子项目?

Dan*_*Lip 4 c# unity-game-engine

这就是我声明列表的方式:

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEditorInternal;

public class ConversationTrigger : MonoBehaviour
{
    public List<Conversation> conversations = new List<Conversation>();
Run Code Online (Sandbox Code Playgroud)

这是使用此 List 的编辑器脚本:

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;

[CustomEditor(typeof(ConversationTrigger))]
public class ConversationTriggerEditor : Editor
{
    private ReorderableList ConversationsList;

    private void OnEnable()
    {
        ConversationTrigger conversationtrigger = (ConversationTrigger)target;
        ConversationsList = new ReorderableList(serializedObject, serializedObject.FindProperty("conversations"), true, true, true, true); 
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        ConversationsList.DoLayoutList();
        serializedObject.ApplyModifiedProperties();
        EditorUtility.SetDirty(target);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是使用 ReorderableList 之前 Inspector 的屏幕截图。我可以单击一个对话项目并将其折叠,然后单击每个子项以再次折叠它,依此类推:

使用 ReorderableList 之前

这个屏幕截图是使用 ReorderableList 时的:

使用 ReorderableList 时

现在我只能拖动项目并更改位置,当单击 + 时,它将添加最后一个项目的重复项。

我现在想做的至少有三件事:

  1. 选择现有项目(例如“Opening”)时,如果单击“+”,则在“Opening”下添加一个新的空对话项目。如果我选择了“魔法”,则在末尾添加一个空的新项目,如果选择了中间“锁定房间”中的一个,则在其后添加一个空项目。这个想法是在所选项目之后添加空项目。

  2. 删除与所选项目相同的项目时

  3. 当双击其中一个项目或制作一个箭头来折叠项目并显示所有子项时,我该如何做到这一点,就像我的问题中的屏幕截图(如“打开”等)?

der*_*ugo 5

我花了很长时间,但我喜欢 EditorScripting :D

假设您的数据结构如下所示

public class ConversationTrigger : MonoBehaviour
{
    public List<Conversation> conversations;

    public void SaveConversations() { }

    public void LoadConversations() { }
}

[Serializable]
public class Conversation
{
    public string Name;
    public bool Foldout;
    public List<Dialogue> Dialogues;
}

[Serializable]
public class Dialogue
{
    public string Name;
    public bool Foldout;
    public List<string> Sentences;
}
Run Code Online (Sandbox Code Playgroud)

我想出了以下编辑器脚本。这个想法基于我之前的一个问题:How to select elements in Nested reordeablelist

对于 EditorScript 来说,它变得非常复杂。我尝试发表很多评论,但如果有任何不清楚的地方,请在评论中询问我。

这是一个巨大的遗憾ReorderableList没有记录在案的功能,因为它非常强大和有用......

有很多事情你必须重写:

  • drawHeaderCallback
  • drawElementCallback
  • elementHeightCallback
  • onAddCallback

为了能够与它们嵌套,将不同的ReorderableLists 存储在字典中:

[CustomEditor(typeof(ConversationTrigger))]
public class ConversationTriggerEditor : Editor
{
    private ConversationTrigger _conversationTrigger;

    [SerializeField] private ReorderableList conversationsList;

    private SerializedProperty _conversations;

    private int _currentlySelectedConversationIndex = -1;

    private readonly Dictionary<string, ReorderableList> _dialoguesListDict = new Dictionary<string, ReorderableList>();
    private readonly Dictionary<string, ReorderableList> _sentencesListDict = new Dictionary<string, ReorderableList>();

    private void OnEnable()
    {
        _conversationTrigger = (ConversationTrigger)target;
        _conversations = serializedObject.FindProperty("conversations");

        conversationsList = new ReorderableList(serializedObject, _conversations)
        {
            displayAdd = true,
            displayRemove = true,
            draggable = true,

            drawHeaderCallback = DrawConversationsHeader,

            drawElementCallback = DrawConversationsElement,

            onAddCallback = (list) =>
            {
                SerializedProperty addedElement;
                // if something is selected add after that element otherwise on the end
                if (_currentlySelectedConversationIndex >= 0)
                {
                    list.serializedProperty.InsertArrayElementAtIndex(_currentlySelectedConversationIndex + 1);
                    addedElement = list.serializedProperty.GetArrayElementAtIndex(_currentlySelectedConversationIndex + 1);
                }
                else
                {
                    list.serializedProperty.arraySize++;
                    addedElement = list.serializedProperty.GetArrayElementAtIndex(list.serializedProperty.arraySize - 1);
                }

                var name = addedElement.FindPropertyRelative("Name");
                var foldout = addedElement.FindPropertyRelative("Foldout");
                var dialogues = addedElement.FindPropertyRelative("Dialogues");

                name.stringValue = "";
                foldout.boolValue = true;
                dialogues.arraySize = 0;
            },

            elementHeightCallback = (index) =>
            {
                return GetConversationHeight(_conversations.GetArrayElementAtIndex(index));
            }
        };
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();

        // if there are no elements reset _currentlySelectedConversationIndex
        if (conversationsList.serializedProperty.arraySize - 1 < _currentlySelectedConversationIndex) _currentlySelectedConversationIndex = -1;

        conversationsList.DoLayoutList();

        if (GUILayout.Button("Save Conversations"))
        {
            _conversationTrigger.SaveConversations();
        }

        if (GUILayout.Button("Load Conversations"))
        {
            Undo.RecordObject(_conversationTrigger, "Loaded conversations from JSON");
            _conversationTrigger.LoadConversations();
        }

        serializedObject.ApplyModifiedProperties();
    }

    #region Drawers

    #region List Headers

    private void DrawConversationsHeader(Rect rect)
    {
        EditorGUI.LabelField(rect, "Conversations");
    }

    private void DrawDialoguesHeader(Rect rect)
    {
        EditorGUI.LabelField(rect, "Dialogues");
    }

    private void DrawSentencesHeader(Rect rect)
    {
        EditorGUI.LabelField(rect, "Sentences");
    }

    #endregion List Headers

    #region Elements

    private void DrawConversationsElement(Rect rect, int index, bool isActive, bool isFocused)
    {
        if (isActive) _currentlySelectedConversationIndex = index;

        var conversation = _conversations.GetArrayElementAtIndex(index);

        var position = new Rect(rect);

        var name = conversation.FindPropertyRelative("Name");
        var foldout = conversation.FindPropertyRelative("Foldout");
        var dialogues = conversation.FindPropertyRelative("Dialogues");
        string dialoguesListKey = conversation.propertyPath;

        EditorGUI.indentLevel++;
        {
            // make the label be a foldout
            foldout.boolValue = EditorGUI.Foldout(new Rect(position.x, position.y, 10, EditorGUIUtility.singleLineHeight), foldout.boolValue, foldout.boolValue ? "" : name.stringValue);

            if (foldout.boolValue)
            {
                // draw the name field
                name.stringValue = EditorGUI.TextField(new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight), name.stringValue);
                position.y += EditorGUIUtility.singleLineHeight;

                if (!_dialoguesListDict.ContainsKey(dialoguesListKey))
                {
                    // create reorderabl list and store it in dict
                    var dialoguesList = new ReorderableList(conversation.serializedObject, dialogues)
                    {
                        displayAdd = true,
                        displayRemove = true,
                        draggable = true,

                        drawHeaderCallback = DrawDialoguesHeader,

                        drawElementCallback = (convRect, convIndex, convActive, convFocused) => { DrawDialoguesElement(_dialoguesListDict[dialoguesListKey], convRect, convIndex, convActive, convFocused); },

                        elementHeightCallback = (dialogIndex) =>
                        {
                            return GetDialogueHeight(_dialoguesListDict[dialoguesListKey].serializedProperty.GetArrayElementAtIndex(dialogIndex));
                        },

                        onAddCallback = (list) =>
                        {
                            list.serializedProperty.arraySize++;
                            var addedElement = list.serializedProperty.GetArrayElementAtIndex(list.serializedProperty.arraySize - 1);

                            var newDialoguesName = addedElement.FindPropertyRelative("Name");
                            var newDialoguesFoldout = addedElement.FindPropertyRelative("Foldout");
                            var sentences = addedElement.FindPropertyRelative("Sentences");

                            newDialoguesName.stringValue = "";
                            newDialoguesFoldout.boolValue = true;
                            sentences.arraySize = 0;
                        }
                    };
                    _dialoguesListDict[dialoguesListKey] = dialoguesList;
                }

                _dialoguesListDict[dialoguesListKey].DoList(new Rect(position.x, position.y, position.width, position.height - EditorGUIUtility.singleLineHeight));
            }

        }
        EditorGUI.indentLevel--;
    }

    private void DrawDialoguesElement(ReorderableList list, Rect rect, int index, bool isActive, bool isFocused)
    {
        if (list == null) return;

        var dialog = list.serializedProperty.GetArrayElementAtIndex(index);

        var position = new Rect(rect);

        var foldout = dialog.FindPropertyRelative("Foldout");
        var name = dialog.FindPropertyRelative("Name");

        {
            // make the label be a foldout
            foldout.boolValue = EditorGUI.Foldout(new Rect(position.x, position.y, 10, EditorGUIUtility.singleLineHeight), foldout.boolValue, foldout.boolValue ? "" : name.stringValue);

            var sentencesListKey = dialog.propertyPath;
            var sentences = dialog.FindPropertyRelative("Sentences");

            if (foldout.boolValue)
            {
                // draw the name field
                name.stringValue = EditorGUI.TextField(new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight), name.stringValue);
                position.y += EditorGUIUtility.singleLineHeight;

                if (!_sentencesListDict.ContainsKey(sentencesListKey))
                {
                    // create reorderabl list and store it in dict
                    var sentencesList = new ReorderableList(sentences.serializedObject, sentences)
                    {
                        displayAdd = true,
                        displayRemove = true,
                        draggable = true,

                        // header for the dialog list
                        drawHeaderCallback = DrawSentencesHeader,

                        // how a sentence is displayed
                        drawElementCallback = (sentenceRect, sentenceIndex, sentenceIsActive, sentenceIsFocused) =>
                        {
                            var sentence = sentences.GetArrayElementAtIndex(sentenceIndex);

                            // draw simple textArea for sentence
                            sentence.stringValue = EditorGUI.TextArea(sentenceRect, sentence.stringValue);
                        },

                        // Sentences have simply a fixed height of 2 lines
                        elementHeight = EditorGUIUtility.singleLineHeight * 2,

                        // when a sentence is added
                        onAddCallback = (sentList) =>
                        {
                            sentList.serializedProperty.arraySize++;
                            var addedElement = sentList.serializedProperty.GetArrayElementAtIndex(sentList.serializedProperty.arraySize - 1);      

                            addedElement.stringValue = "";
                        }
                    };

                    // store the created ReorderableList
                    _sentencesListDict[sentencesListKey] = sentencesList;
                }

                // Draw the list
                _sentencesListDict[sentencesListKey].DoList(new Rect(position.x, position.y, position.width, position.height - EditorGUIUtility.singleLineHeight));
            }
        }
    }

    #endregion Elements

    #endregion Drawers


    #region Helpers

    #region HeightGetter

    /// <summary>
    /// Returns the height of given Conversation property
    /// </summary>
    /// <param name="conversation"></param>
    /// <returns>height of given Conversation property</returns>
    private float GetConversationHeight(SerializedProperty conversation)
    {
        var foldout = conversation.FindPropertyRelative("Foldout");

        // if not foldout the height is simply 1 line
        var height = EditorGUIUtility.singleLineHeight;

        // otherwise we sum up every controls and child heights
        if (foldout.boolValue)
        {
            // we need some more lines:
            //  for the Name field,
            // the list header,
            // the list buttons and a bit buffer
            height += EditorGUIUtility.singleLineHeight * 5;


            var dialogues = conversation.FindPropertyRelative("Dialogues");

            for (var d = 0; d < dialogues.arraySize; d++)
            {
                var dialog = dialogues.GetArrayElementAtIndex(d);
                height += GetDialogueHeight(dialog);
            }
        }

        return height;
    }

    /// <summary>
    /// Returns the height of given Dialogue property
    /// </summary>
    /// <param name="dialog"></param>
    /// <returns>height of given Dialogue property</returns>
    private float GetDialogueHeight(SerializedProperty dialog)
    {
        var foldout = dialog.FindPropertyRelative("Foldout");

        // same game for the dialog if not foldout it is only a single line
        var height = EditorGUIUtility.singleLineHeight;

        // otherwise sum up controls and child heights
        if (foldout.boolValue)
        {
            // we need some more lines:
            //  for the Name field,
            // the list header,
            // the list buttons and a bit buffer
            height += EditorGUIUtility.singleLineHeight * 4;

            var sentences = dialog.FindPropertyRelative("Sentences");

            // the sentences are easier since they always have the same height
            // in this example 2 lines so simply do
            // at least have space for 1 sentences even if there is none
            height += EditorGUIUtility.singleLineHeight * Mathf.Max(1, sentences.arraySize) * 2;
        }

        return height;
    }

    #endregion

    #endregion Helpers
}
Run Code Online (Sandbox Code Playgroud)

添加新的对话,给它们命名,折叠它们。如果选择了一个对话,则在其后添加新对话

在此输入图像描述

添加对话和句子,并且仍然能够重新排列和(展开)折叠所有内容

在此输入图像描述