Unity的Reorderable List用法
关于ReorderableList
Unity官方文档里完全没有提到ReorderableList
类,这是因为它不在UnityEngine
或UnityEditor
的命名空间下,而是在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的高度 |
onAddCallback | element添加时的回调 |
onRemoveCallback | element移除时的回调 |
还有更多的delegate,可以从Unity的C# source code里看到。
为了自定义Inspector的UI内容,需要使用到drawElementCallback
和drawHeaderCallback
两个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