In Unity 4.5 we got a nice (undocumented) built-in tool to visualize lists in IDE. It’s called ReorderableList
, it’s located in UnityEditorInternal
namespace and looks like this:
在 Unity 4.5 中,我们得到了一个很好的(未记录的)内置工具,可以在 IDE 中可视化列表。它称为 ReorderableList
,位于 UnityEditorInternal
命名空间中,如下所示:
Let’s see how to code this beautiful interface in Unity IDE using ReorderableList.
让我们看看如何使用 ReorderableList 在 Unity IDE 中编写这个漂亮的界面。
Note: Though UnityEditorInternal namespace is public, it seems to be intended for Unity Team internal use (hence the name), it is not documented and might change in future versions of Unity. If you notice an API change please post it in comments section below.
注意:尽管 UnityEditorInternal 命名空间是公共的,但它似乎旨在供 Unity 团队内部使用(因此得名),但它没有记录在案,并且在 Unity 的未来版本中可能会发生变化。如果您注意到 API 更改,请在下面的评论部分发布。
Sources:Project sources are on GitHub.
来源:项目来源位于 GitHub 上。
Setting up the project 设置项目
Let’s pretend that we are making a Tower Defense-like game and need a decent interface for our game-designer to set up waves of monsters for a particular level.
让我们假设我们正在制作一个类似塔防的游戏,并且需要一个像样的界面供我们的游戏设计师为特定级别设置一波又一波的怪物。
First, create a new Unity project.
首先,创建一个新的 Unity 项目。
We will need some test assets: mobs and bosses. We’ll be placing them in Prefabs/Mobs
and Prefabs/Bosses
folders. Create several prefabs and put them in these folders. Right now these can be just default cubes and spheres. Give them descriptive names. This is what I came up with:
我们需要一些测试资源:生物和 Boss。我们将把它们放在 Prefabs/Mobs
和 Prefabs/Bosses
文件夹中。创建多个预制件并将其放入这些文件夹中。现在,这些可能只是默认的立方体和球体。为它们指定描述性名称。这是我想出的:
Storing data 存储数据
Create Scripts
folder where we will store our C# scripts. Create two scripts in this folder: LevelData.cs
and MobWave.cs
.
创建 Scripts
文件夹,我们将在其中存储 C# 脚本。在此文件夹中创建两个脚本:LevelData.cs
和 MobWave.cs
。
Add this code to MobWave.cs
. This is our value object for storing data for a wave of monsters.
将此代码添加到 MobWave.cs
。这是我们为一波怪物存储数据的值对象。
using UnityEngine;
using System;
[Serializable]
public struct MobWave {
public enum WaveType {
Mobs,
Boss
}
public WaveType Type;
public GameObject Prefab;
public int Count;
}
As you see, every wave can be either a wave of Mobs or one or more Bosses. Every wave has a link to a prefab to clone and a number of copies.
如你所见,每一波都可以是一波生物,也可以是一个或多个 Boss。每个 wave 都有一个指向要克隆的预制件的链接和多个副本。
Note: Unity can serialize custom structs since version 4.5.
注意:Unity 从 4.5 版本开始可以序列化自定义结构。
Add this code to LevelData.cs
. This is just a container for our MobWave
objects.
将此代码添加到 LevelData.cs
。这只是我们的 MobWave
对象的一个容器。
using UnityEngine;
using System.Collections.Generic;
public class LevelData : MonoBehaviour {
public List<MobWave> Waves = new List<MobWave>();
}
Add a GameObject
to your scene and call it Data
. Add LevelData
component to this game object and set it up as shown in the GIF animation below.
将游戏对象
添加到场景中,并将其命名为 Data
。将 LevelData
组件添加到此游戏对象中,并按照下面的 GIF 动画进行设置。
This is basically how lists in Unity IDE look by default. Of course they are not bad, but they become a major pain very quickly when you have many items in a list and want to move them around and add some in the middle.
这基本上就是 Unity IDE 中列表的默认外观。当然,它们还不错,但是当您列表中有许多项目并希望移动它们并在中间添加一些时,它们很快就会成为一个主要的痛苦。
Building a custom inspector
构建自定义检查器
For every Component in Unity IDE you can create a custom inspector which will change how the component is shown in Inspector tab. Right now we will do this for our LevelData.cs
script to change Unity’s default list rendering to more functional ReorderableList.
对于 Unity IDE 中的每个组件,您可以创建自定义检查器,这将更改组件在 Inspector 选项卡中的显示方式。现在,我们将对 LevelData.cs
脚本执行此操作,以将 Unity 的默认列表渲染更改为功能更强大的 ReorderableList。
Create Editor
folder and a new script inside. Call it LevelDataEditor.cs
.
Create Editor
文件夹和内部的新脚本。称之为 LevelDataEditor.cs
。
Note: All custom inspectors must be in Editor folder and inherit from
Editor
class.
注意:所有自定义检查器都必须位于 Editor 文件夹中,并继承自Editor
类。
Add this code to the new script:
将此代码添加到新脚本中:
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;
[CustomEditor(typeof(LevelData))]
public class LevelDataEditor : Editor {
private ReorderableList list;
private void OnEnable() {
list = new ReorderableList(serializedObject,
serializedObject.FindProperty("Waves"),
true, true, true, true);
}
public override void OnInspectorGUI() {
serializedObject.Update();
list.DoLayoutList();
serializedObject.ApplyModifiedProperties();
}
}
All custom inspector classes have the same important parts:
所有自定义 Inspector 类都有相同的重要部分:
- A custom inspector must inherit from
Editor
class,
自定义检查器必须继承自Editor
类 - To tell Unity that this is an inspector for
LevelData
component we must add[CustomEditor(typeof(LevelData))]
attribute,
要告诉 Unity 这是LevelData
组件的检查器,我们必须添加[CustomEditor(typeof(LevelData))]
属性 private void OnEnable()
method is used for initialization,private void OnEnable()
方法用于初始化,public override void OnInspectorGUI()
method is called when inspector is redrawn.public override void OnInspectorGUI()
方法。
In our case in OnEnable
we are creating an instance of ReorderableList
to draw our Waves
property. Don’t forget to import UnityEditorInternal namespace:
在本例中,在 OnEnable
中,我们将创建一个 ReorderableList
实例来绘制我们的 Waves
属性。不要忘记导入 UnityEditorInternal 命名空间:
using UnityEditorInternal;
ReorderableList works with standard C# lists as well as with SerializedProperties. It has two constructors and their more general variants:
ReorderableList 适用于标准 C# 列表以及 SerializedProperties。它有两个构造函数及其更通用的变体:
public ReorderableList(IList elements, Type elementType)
,public ReorderableList(SerializedObject serializedObject, SerializedProperty elements)
.
In this example we are using SerializedProperty because this is the recommended way of working with properties in custom inspectors. This makes the code smaller and works nicely with Unity and Undo system.
在此示例中,我们使用 SerializedProperty,因为这是在自定义检查器中使用属性的推荐方法。这使得代码更小,并且可以很好地与 Unity 和 Undo 系统配合使用。
public ReorderableList(
SerializedObject serializedObject,
SerializedProperty elements,
bool draggable,
bool displayHeader,
bool displayAddButton,
bool displayRemoveButton);
As you see just by using right parameters we can restrict adding, removing and reordering of items in our list.
正如你所看到的,只需使用正确的参数,我们就可以限制列表中项目的添加、删除和重新排序。
Later in the code we just call list.DoLayoutList()
to draw the interface.
稍后在代码中,我们只调用 list。DoLayoutList()
来绘制接口。
Right now you are probably saying: “Wait, this looks even worse!”. But wait a minute, our data structure is complex so Unity doesn’t know how to draw it properly. Let’s fix this.
现在你可能会说:“等等,这看起来更糟!但是等一下,我们的数据结构很复杂,所以 Unity 不知道如何正确绘制它。让我们来解决这个问题。
Drawing list items 图纸列表项
ReorderableList exposes several delegates we can use to customize our lists. The first one is drawElementCallback
. It’s called when a list item is drawn.
ReorderableList 公开了几个我们可以用来自定义列表的委托。第一个是 drawElementCallback
。它在绘制列表项时调用。
Add this code in OnEnable
after the list is created:
创建列表后,在 OnEnable
中添加以下代码:
list.drawElementCallback =
(Rect rect, int index, bool isActive, bool isFocused) => {
var element = list.serializedProperty.GetArrayElementAtIndex(index);
rect.y += 2;
EditorGUI.PropertyField(
new Rect(rect.x, rect.y, 60, EditorGUIUtility.singleLineHeight),
element.FindPropertyRelative("Type"), GUIContent.none);
EditorGUI.PropertyField(
new Rect(rect.x + 60, rect.y, rect.width - 60 - 30, EditorGUIUtility.singleLineHeight),
element.FindPropertyRelative("Prefab"), GUIContent.none);
EditorGUI.PropertyField(
new Rect(rect.x + rect.width - 30, rect.y, 30, EditorGUIUtility.singleLineHeight),
element.FindPropertyRelative("Count"), GUIContent.none);
};
Return to Unity. Now all the list items are pretty and functional. Try adding new ones and dragging them around. This is much better, right?
返回 Unity。现在所有列表项都很漂亮且功能齐全。尝试添加新的并拖动它们。这要好得多,对吧?
If you are not familiar with the syntax and APIs in the code you just pasted, this is a standard C#lambda-expression with Editor GUI methods.
如果您不熟悉刚刚粘贴的代码中的语法和 API,这是带有编辑器 GUI 方法的标准 C#lambda 表达式。
Note: It’s important to have a semicolon at the end of a lambda };
注意:在 lambda } 的末尾有一个分号很重要;
Here we are getting the list item being drawn:
在这里,我们得到了正在绘制的列表项:
var element = list.serializedProperty.GetArrayElementAtIndex(index);
We are using FindPropertyRelative
method to find properties of the wave:
我们使用 FindPropertyRelative
方法来查找波形的属性:
element.FindPropertyRelative("Type")
And after that we are drawing 3 properties in one line: Type
, Prefab
and Count
:
之后,我们在一行中绘制 3 个属性:Type
、Prefab
和 Count
:
EditorGUI.PropertyField(
new Rect(rect.x, rect.y, 60, EditorGUIUtility.singleLineHeight),
element.FindPropertyRelative("Type"), GUIContent.none);
Note that list header says “Serialized Property”. Let’s change it to something more informative. To do this we need to use drawHeaderCallback
.
请注意,列表标题显示 “Serialized Property”。让我们将其更改为信息量更大的内容。为此,我们需要使用 drawHeaderCallback
。
Paste this code in OnEnable
method:
将此代码粘贴到 OnEnable
方法中:
list.drawHeaderCallback = (Rect rect) => {
EditorGUI.LabelField(rect, "Monster Waves");
};
Now it feels right. 现在感觉是对的。
At this stage the list is fully functional but I want to show you how we can extend it even further. Let’s see what other callbacks ReorderableList has to offer.
在这个阶段,这个列表功能齐全,但我想向您展示我们如何进一步扩展它。让我们看看 ReorderableList 还提供了哪些其他回调。
Callbacks 回调
Here are all the callbacks exposed by an instance of ReorderableList:
以下是 ReorderableList 实例公开的所有回调:
- drawElementCallback drawElement回调
- drawHeaderCallback drawHeader回调
- onReorderCallback onReorder回调
- onSelectCallback onSelect回调
- onAddCallback onAddCallback 回调
- onAddDropdownCallback onAddDropdown回调
- onRemoveCallback onRemoveCallback 回调
- onCanRemoveCallback onCanRemove回调
- onChangedCallback onChangedCallback 回调
drawElementCallback drawElement回调
Signature: (Rect rect, int index, bool isActive, bool isFocused)
签名: (Rect rect, int index, bool isActive, bool isFocused)
Here you can specify exactly how your list elements must be drawn. Because if you don’t this code is used to draw list elements:
在这里,您可以准确指定必须如何绘制列表元素。因为如果你不这样做,这段代码将用于绘制列表元素:
EditorGUI.LabelField(rect,
EditorGUIUtility.TempContent((element == null) ? listItem.ToString() : element.displayName));
Signature: (Rect rect)
签名: (Rect rect)
Is used to draw list header.
用于绘制列表标题。
onReorderCallback onReorder回调
Signature: (ReorderableList list)
Called when an element is moved in the list.
onSelectCallback
Signature: (ReorderableList list)
Called when an element is selected in the list.
onAddCallback
Signature: (ReorderableList list)
Called when the + button is pressed. If this callback is assigned it must create an item itself, in this case default logic is disabled.
onAddDropdownCallback
Signature: (Rect buttonRect, ReorderableList list)
Called when the + button is pressed. If this callback is assigned + button changes to Add more button and onAddCallback
is ignored. As with onAddCallback
you must create a new list element yourself.
onRemoveCallback
Signature: (ReorderableList list)
Called to remove selected element from the list. If this callback is defined default logic is disabled.
onCanRemoveCallback
Signature: bool (ReorderableList list)
Called when — button is drawn to determine if it should be active or disabled.
onChangedCallback
Signature: (ReorderableList list)
Called when the list is changed, i.e. an item added, removed or rearranged. If data within an item is changed this callback is not called.
Adding selection helper
You can add Debug.Log()
to all these callbacks to see when they are called. But now let’s make something useful with some of them.
The first one will be onSelectCallback
. We’ll make that when you select an element the corresponding mob prefab is highlighted in Project panel.
Add this code to OnEnable
method:
list.onSelectCallback = (ReorderableList l) => {
var prefab = l.serializedProperty.GetArrayElementAtIndex(l.index).FindPropertyRelative("Prefab").objectReferenceValue as GameObject;
if (prefab)
EditorGUIUtility.PingObject(prefab.gameObject);
};
The code is pretty simple. We are looking for Prefab
property of selected wave and if it’s defined we are calling EditorGUIUtility.PingObject
method to highlight it in Project panel.
Checking how many waves have left
Let’s say that we want to have at least one wave in our list at all times. In other words we want to disable — button if there’s only one element in the list. We can do this using onCanRemoveCallback
.
Add this code to OnEnable
method:
list.onCanRemoveCallback = (ReorderableList l) => {
return l.count > 1;
};
Now try deleting all elements from the list. You will see that if there’s only one element the — button is disabled.
Adding a warning
We don’t really want to accidentally delete an item in our list. Using onRemoveCallback
we can display a warning and make a user press Yes button if he really wants to delete an element.
list.onRemoveCallback = (ReorderableList l) => {
if (EditorUtility.DisplayDialog("Warning!",
"Are you sure you want to delete the wave?", "Yes", "No")) {
ReorderableList.defaultBehaviours.DoRemoveButton(l);
}
};
Note how we used ReorderableList.defaultBehaviours.DoRemoveButton
method here. ReorderableList.defaultBehaviours
contains all default implementations for various functions which sometimes can be handy if you don’t want to reinvent the wheel.
Initializing a newly created element
What if we wanted to add a preconfigured element when a user presses + button instead of copying the last one in the list? We can intercept the logic of adding elements using onAddCallback
.
Add the following code to OnEnable
method:
list.onAddCallback = (ReorderableList l) => {
var index = l.serializedProperty.arraySize;
l.serializedProperty.arraySize++;
l.index = index;
var element = l.serializedProperty.GetArrayElementAtIndex(index);
element.FindPropertyRelative("Type").enumValueIndex = 0;
element.FindPropertyRelative("Count").intValue = 20;
element.FindPropertyRelative("Prefab").objectReferenceValue =
AssetDatabase.LoadAssetAtPath("Assets/Prefabs/Mobs/Cube.prefab",
typeof(GameObject)) as GameObject;
};
Here we are adding an empty wave to the end of the list and with the help of element.FindPropertyRelative
method we are setting its properties to predefined values. In case of Prefab
property we are looking for the specific prefab at path Assets/Prefabs/Mobs/Cube.prefab
. Make sure that you created the folder structure accordingly.
Now return to Unity and try adding new elements to the list. You will see that new elements are always added as Cubes with Count equal to 20.
The last example will be the most interesting. We will make a dynamic drop-down menu activated by + button using onAddDropdownCallback
.
In Prefabs
folder we have 3 mobs and 3 bosses, so it would be natural to be able to add a specific one from a menu instead of manually dragging prefabs in the list.
Let’s first add a new data type which we will later use in our menu system. Add this code at the end of the file before the last }.
private struct WaveCreationParams {
public MobWave.WaveType Type;
public string Path;
}
Next, define a callback which will be called by built-in Unity menu system. Right now it’s empty but we’ll fix this later. Add the following code after OnInspectorGUI
method.
private void clickHandler(object target) {}
And now is the time to define our onAddDropdownCallback
. Add this code in OnEnable
method:
list.onAddDropdownCallback = (Rect buttonRect, ReorderableList l) => {
var menu = new GenericMenu();
var guids = AssetDatabase.FindAssets("", new[]{"Assets/Prefabs/Mobs"});
foreach (var guid in guids) {
var path = AssetDatabase.GUIDToAssetPath(guid);
menu.AddItem(new GUIContent("Mobs/" + Path.GetFileNameWithoutExtension(path)),
false, clickHandler,
new WaveCreationParams() {Type = MobWave.WaveType.Mobs, Path = path});
}
guids = AssetDatabase.FindAssets("", new[]{"Assets/Prefabs/Bosses"});
foreach (var guid in guids) {
var path = AssetDatabase.GUIDToAssetPath(guid);
menu.AddItem(new GUIContent("Bosses/" + Path.GetFileNameWithoutExtension(path)),
false, clickHandler,
new WaveCreationParams() {Type = MobWave.WaveType.Boss, Path = path});
}
menu.ShowAsContext();
};
Here we are building dynamic drop-down menu from the files in Mobs and Bosses folders, assigning instances of WaveCreationParams
to them to be able to find out what menu element was clicked later in clickHandler
.
If you set up your folder structure right and didn’t forget to add using System.IO;
in the beginning of the file, you can now return to Unity and try pressing + button (which is now Plus More) to see how it works.
Now all what’s left is to add actual wave creation logic to clickHandler
:
private void clickHandler(object target) {
var data = (WaveCreationParams)target;
var index = list.serializedProperty.arraySize;
list.serializedProperty.arraySize++;
list.index = index;
var element = list.serializedProperty.GetArrayElementAtIndex(index);
element.FindPropertyRelative("Type").enumValueIndex = (int)data.Type;
element.FindPropertyRelative("Count").intValue =
data.Type == MobWave.WaveType.Boss ? 1 : 20;
element.FindPropertyRelative("Prefab").objectReferenceValue =
AssetDatabase.LoadAssetAtPath(data.Path, typeof(GameObject)) as GameObject;
serializedObject.ApplyModifiedProperties();
}
When a menu item is clicked this method is called with the value object we specified earlier. This value object containing in data
variable is used to set up properties of the newly created wave. To assing the right Prefab to the wave we are using AssetDatabase.LoadAssetAtPath(data.Path, typeof(GameObject))
method with the path we found in onAddDropdownCallback
.
In conclusion
Of the all available callbacks we haven’t touched only onChangedCallback
and onReorderCallback
because they are not really interesting. But you must know that they exist.
If you’ve been working with Unity for a long time you should know how hard it is to make a proper interface for a collection of things in Unity IDE. Especially when this wasn’t in your time budget from the beginning. I’ve been using another implementation of ReorderableList by rotorz for a while. But now when we have an implementation from Unity Team there’s no excuse not to use it.
If you want to find out how this list is implemented you can use ILSpy to decompile UnityEditor.dll which is (thankfully) not obfuscated or otherwise protected.
May the pretty lists be with you!
P.S. Alexei in comments proposed a solution to list item height problem which is described here: https://feedback.unity3d.com/suggestions/custom-element-size-in-reorderable-list
Code: http://pastebin.com/WhfRgcdC
来自: Unity:使用 ReorderableList 使您的列表正常运行 —- Unity: make your lists functional with ReorderableList