Unity的Reorderable List用法

关于ReorderableList

Unity官方文档里完全没有提到ReorderableList类,这是因为它不在UnityEngineUnityEditor的命名空间下,而是在UnityEditorInternal命名空间下,这个命名空间里的东西是没有官方文档支持的。


ReorderableList的作用

它的作用,是让一个数组,在Unity的Inspector界面上显示得更好一些。

举个例子,如果我一个MonoBehaviour脚本,里面有个数组,两种写法都可:

// 写法一
// 注意Wave是个struct, 后面会再提
public Wave[] wave;

// 写法二
public List<Wave> wave;

这两种结果,在Inspector上显示效果都是一样的,如下图所示:
在这里插入图片描述
这种List布局有以下缺点:

  • 无法手动改变这些元素的顺序
  • 如果要添加新元素,要改变上面的size,然后填新的值
  • 如果要删除其中一个元素,那么不太好做

而有了ReorderableList,就可以解决这些问题,用了它以后,Inspector上的数组UI会变成这样,红色区域的东西可以用于拖拽改变元素顺序:
在这里插入图片描述


具体的代码实现方法

首先是还没使用ReorderableList的代码,此时的Inspector就是老旧的数组形式:

// Wave.cs文件
[System.Serializable]
public struct Wave					// 塔防游戏里代表一波进攻的敌人
{
    // Declares the Mobs enum type. Mob翻译为暴民
    public enum Mobs
    {
        Goblin, 
        Slime, 
        Bat
    }

    public Mobs mobs; // What kind of enemy should be spawned in this wave?
    public int level; // Level of the enemy.
    public int quantity; // How many enemies of this type should we spawn?
}

// WaveManager.cs文件
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WaveManager : MonoBehaviour
{
     public Wave[] wave;
}

使用ReorderableList后,代码应该这么写:

// 创建一个新的WaveManagerEditor.cs文件
using UnityEditor;
using UnityEditorInternal;

// 还是传统的更改Inspector UI的方式, 就是创建一个继承Editor的类, 然后加个CustomEditor的Attribute
// Tells Unity to use this Editor class with the WaveManager script component.
[CustomEditor(typeof(WaveManager))]
public class WaveManagerEditor : Editor// Editor类是ScriptableObject类的派生类
{
    // 重点一: 用于存储WaveManager里的wave数组(应该是存的引用)
    SerializedProperty wave;  
    // 重点二: 声明ReorderableList对象
    ReorderableList list; 

	// 初始化步骤
    private void OnEnable()
    {
    	// 从WaveManager里取Wave数组给wave赋值
    	wave = serializedObject.FindProperty("wave");// 变量名字叫wave
		
		// new一个ReorderableList
   		list = new ReorderableList(serializedObject, wave, true, true, true, true);		
    }

    // This is the function that makes the custom editor work
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();// 暂时先绘制原本的内容

		// 当ReorderableList的UI上产生任何改变时, 将值的变化应用到wave对应的array上
    }
}

目前的代码,虽然创建了Reorderable List,但是仍然调用的base.OnInspectorGUI(),此时Inspector上的UI是还没改变的。


如何绘制ReorderableList

ReorderableList提供了很多delegate,用于帮助绘制UI,比如这些:

Callback delegate描述
drawElementCallback用于绘制list里的每个element
drawHeaderCallback用于绘制header
elementHeightCallback用于设置每个element的高度
onAddCallbackelement添加时的回调
onRemoveCallbackelement移除时的回调

还有更多的delegate,可以从Unity的C# source code里看到。

为了自定义Inspector的UI内容,需要使用到drawElementCallbackdrawHeaderCallback两个delegate,代码如下所示:

private void OnEnable()
{
    wave = serializedObject.FindProperty("wave");
    list = new ReorderableList(serializedObject, wave, true, true, true, true);

    list.drawHeaderCallback = DrawHeader; // Skip this line if you set displayHeader to 'false' in your ReorderableList constructor.
    list.drawElementCallback = DrawListItems; // Delegate to draw the elements on the list
}

//Draws the header
void DrawHeader(Rect rect)
{
	string name = "Wave";
    EditorGUI.LabelField(rect, name);
}

// Draws the elements on the list, 这个函数应该会为每个list里的Element都调用一次
void DrawListItems(Rect rect, int index, bool isActive, bool isFocused)
{
	// 获取第index个element
    SerializedProperty element = list.serializedProperty.GetArrayElementAtIndex(index);

    // 为每个property绘制一个property field和label field

    //The 'mobs' property. Since the enum is self-evident, I am not making a label field for it. 
    //The property field for mobs (width 100, height of a single line)
    EditorGUI.PropertyField(
        new Rect(rect.x, rect.y, 100, EditorGUIUtility.singleLineHeight), 
        element.FindPropertyRelative("mobs"),
        GUIContent.none
    ); 


    //The 'level' property
    //The label field for level (width 100, height of a single line)
    EditorGUI.LabelField(new Rect(rect.x + 120, rect.y, 100, EditorGUIUtility.singleLineHeight), "Level");

    //The property field for level. Since we do not need so much space in an int, width is set to 20, height of a single line.
    EditorGUI.PropertyField(
        new Rect(rect.x + 160, rect.y, 20, EditorGUIUtility.singleLineHeight),
        element.FindPropertyRelative("level"),
        GUIContent.none
    ); 

    //The 'quantity' property
    //The label field for quantity (width 100, height of a single line)
    EditorGUI.LabelField(new Rect(rect.x + 200, rect.y, 100, EditorGUIUtility.singleLineHeight), "Quantity");

    //The property field for quantity (width 20, height of a single line)
    EditorGUI.PropertyField(
        new Rect(rect.x + 250, rect.y, 20, EditorGUIUtility.singleLineHeight),
        element.FindPropertyRelative("quantity"),
        GUIContent.none
    );        
}

接下来,再去修改OnInspectorGUI函数即可:

public override void OnInspectorGUI()
{
	// 注释这行代码可以把原本的数组给隐藏掉
    // base.OnInspectorGUI();

	// 把array里的数据更新到Inspector上
    serializedObject.Update(); // Update the array property's representation in the inspector
	
	// 看了下C#源码, 这里绘制了Header, Element等内容, 好像是调用了DrawHeader和DrawElements函数
    list.DoLayoutList(); // Have the ReorderableList do its work

	// 把List从Inspector得到的新的信息应用到具体的数据上, serializedObject是
	// 继承的Editor类的数据成员(Editor类其实是个ScriptableObject)
    // We need to call this so that changes on the Inspector are saved by Unity.
    serializedObject.ApplyModifiedProperties();
}

效果如下图所示,红色区域是Header,绿色区域是DrawListItems的内容:
在这里插入图片描述


ReorderableList用于ScriptableObject的Inspector界面

前面的ReorderableList是用在了MonoBehaviour上的Inspector界面,这里介绍第二种使用场景,就是ScriptableObject的Inspector界面。

其实很简单,WaveManagerEditor类的代码完全不需要改动,写法是一样的:

// 把原本的WaveManager改成继承于ScriptableObject
[CreateAssetMenu(menuName = "WaveManager")]
public class WaveManager : ScriptableObject
{
    public Wave[] wave;
}

// 其他的代码完全不变

这样在菜单栏创建出来的ScriptableObject,其Inspector界面也是一样的,如下图所示:
在这里插入图片描述


ReorderableList里存储Array

问题

这里的Wave里的数据是这样,都是单个数据:

public Mobs mobs; // What kind of enemy should be spawned in this wave?
public int level; // Level of the enemy.
public int quantity; // How many enemies of this type should we spawn?

但是我一旦改成这样:

public Mobs[] mobs; // What kind of enemy should be spawned in this wave?
public int level; // Level of the enemy.
public int quantity; // How many enemies of this type should we spawn?

原本的UI就变成了这样:
在这里插入图片描述

就算更改顺序,放到后面,前面拖拽顺序的Icon可以出来,这里也还是不对:
在这里插入图片描述


解决办法

网上搜了下怎么写,这里就直接给一个新的例子代码了,用下面这个例子就可以解决前面说的问题:

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

public class Test : MonoBehaviour
{
    public List<LevelData> levels;

    [System.Serializable]
    public class LevelData
    {
        public string[] names;
    }
}

[CustomEditor(typeof(Test))]
public class TestEditor : Editor
{
    private ReorderableList list;

    private void OnEnable()
    {
        list = new ReorderableList(serializedObject,serializedObject.FindProperty("levels"), true, true, true, true)
        {
            elementHeightCallback = ElementHeightCallback,
            drawElementCallback = DrawListElement
        };
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        list.DoLayoutList();
        serializedObject.ApplyModifiedProperties();
    }

    // 计算每个Element的高度
    private float ElementHeightCallback(int index)
    {
        // Set the height of each row dynamically depending on the height of the names entries.
        // 获取对应的Element
        SerializedProperty element = list.serializedProperty.GetArrayElementAtIndex(index);
        // 获取对应Element里的数组高度
        SerializedProperty namesProp = element.FindPropertyRelative("names");

        // 也就是说, 这种数组Element的高度基于它内部这个names数组的高度
        return EditorGUI.GetPropertyHeight(namesProp) + EditorGUIUtility.standardVerticalSpacing;
    }

    private void DrawListElement(Rect rect, int index, bool isActive, bool isFocused)
    {
        // 获取第Index元素里的数组对应的property
        var element = list.serializedProperty.GetArrayElementAtIndex(index);
        SerializedProperty namesProp =  element.FindPropertyRelative("names");

        // By default, the array dropdown is offset to the left which intersects with
        // the drag handle, so we can either indent the array property or inset the rect.
        EditorGUI.indentLevel++;

        // Take note of the last argument, since this is an array,
        // we want to draw it with all of its children.
        EditorGUI.LabelField(rect, "Example");


        rect.x += 100;
        EditorGUI.PropertyField(rect, namesProp, includeChildren: true);
        EditorGUI.indentLevel--;
    }
}

效果如下图所示:
在这里插入图片描述


重点有两个:

  • 添加ElementHeightCallback函数,在里面把含有数组元素S的高度,用其中的数组的高度来代替(比如说A里有个数组B,那么A的高度其实是B的高度)
  • EditorGUI.PropertyField(rect, namesProp, includeChildren: true);函数里,添加一个bool值,旨在把数组里的子元素给展示出来

新版本的Unity

顺便提一句,新版本的Unity,比如我看的Unity2021.1.12f,里面的数组,已经默认是用Reorderable List进行显示了,还挺方便的,如下图所示:
在这里插入图片描述


参考

参考:How to use ‘ReorderableList’
参考:Unity编辑器拓展之一:ReorderableList可重新排序的列表框(简单使用)
参考:Unity3D: using ReorderableList in Custom Editor
参考:Creating Reorderable Lists in the Unity Inspector
参考:Custom editor: display array within ReorderableList

posted @ 2022-12-03 13:07  弹吉他的小刘鸭  阅读(564)  评论(0编辑  收藏  举报