c# - 如何在检查器中使用带有列表的 ReorderableList 并添加新的空项目和折叠子项?

标签 c# unity3d

这就是我声明 List 的方式:

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>();

这是使用此 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);
    }
}

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

Before using ReorderableList

此屏幕截图是使用 ReorderableList 时的截图:

When using ReorderableList

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

我现在想做的事情我至少能想到三件事:
  • 选择现有项目时,例如 Opening,然后单击 + 在 Opening 下添加一个新的空对话项目。如果我选择了 Magic 在最后添加空的新项目,如果选择了中间 Locked Room 中的一个,在它后面添加空项目。这个想法是在所选项目之后添加空项目。
  • 删除与所选项目相同的项目时
  • 当单击其中一个项目或制作一个箭头来折叠项目并在我的问题(如 Opening 和其他人)中的屏幕截图中显示所有 child 时,我该如何做到这一点?
  • 最佳答案

    我花了很长时间,但我喜欢 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;
    }
    

    我想出了以下 EditorScript。这个想法是基于我在 How to select elements in nested reordeablelist 上的一个以前的问题.

    EditorScript 像往常一样非常完整。我试图发表很多评论,但如果有任何不清楚的地方,请在评论中问我。

    非常遗憾ReorderableList不是记录在案的功能,因为它非常强大和有用......

    您必须覆盖多项内容:
  • drawHeaderCallback
  • drawElementCallback
  • elementHeightCallback
  • onAddCallback

  • 并且为了能够与它们交互,嵌套存储不同的 ReorderableList s 在字典中:
    [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
    }
    

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

    enter image description here

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

    enter image description here

    关于c# - 如何在检查器中使用带有列表的 ReorderableList 并添加新的空项目和折叠子项?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56180821/

    相关文章:

    c# - 使用 ASP.NET Web API 自定义路由

    c# - 是否有类似 ICollection<t> 的接口(interface),但专为排序的集合设计?

    c# - 对象数组未按预期交付

    c# - 如何通过 IEnumerable 使用 TableQuery 进行查询?

    c# - 触摸跳转Unity C#

    unity3d - Unity3D MMORPG的后端服务器

    c# - unity3d如何改变实例化预制件的颜色

    c# - 为什么 WinForms 应用程序默认是 STAThread?

    c# - Unity 中的 Google Firebase 身份验证 : How to read error codes

    c# - 在 unity 3d 中检测物体