[转]Unity: make your lists functional with ReorderableList
2016-05-04 12:47
676 查看
原文地址:http://va.lent.in/unity-make-your-lists-functional-with-reorderablelist/
This article is reproduced, the original address:
http://va.lent.in/unity-make-your-lists-functional-with-reorderablelist/
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 UnityEditorInternalnamespace and looks like this:
Let's see how to code this beautiful interface in Unity IDE usingReorderableList.
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.
Sources: Project sources are on GitHub.
First, create a new Unity project.
We will need some test assets: mobs and bosses. We'll be placing them in
Add this code to
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.
Note: Unity can serialize custom structs since version 4.5.
Add this code to
Add a
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.
Create
Note: All custom inspectors must be in Editor folder and inherit from
Add this code to the new script:
All custom inspector classes have the same important parts:
A custom inspector must inherit from
To tell Unity that this is an inspector for
In our case in
ReorderableList works with standard C# lists as well as withSerializedProperties. It has two constructors and their more general variants:
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.
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
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.
Add this code in
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?
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.
Note: I't important to have a semicolon at the end of a lambda };
Here we are getting the list item being drawn:
We are using
And after that we are drawing 3 properties in one line:
Note that list header says "Serialized Property". Let's change it to something more informative. To do this we need to use
Paste this code in
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.
drawElementCallback
drawHeaderCallback
onReorderCallback
onSelectCallback
onAddCallback
onAddDropdownCallback
onRemoveCallback
onCanRemoveCallback
onChangedCallback
Signature: (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:
Signature: (Rect rect)
Is used to draw list header.
Signature: (ReorderableList list)
Called when an element is moved in the list.
Signature: (ReorderableList list)
Called when an element is selected in the list.
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.
Signature: (Rect buttonRect, ReorderableList list)
Called when the + button is pressed. If this callback is assigned + button changes to Add more button and
Signature: (ReorderableList list)
Called to remove selected element from the list. If this callback is defined default logic is disabled.
Signature: bool (ReorderableList list)
Called when — button is drawn to determine if it should be active or disabled.
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.
The first one will be
Add this code to
The code is pretty simple. We are looking for
Add this code to
Now try deleting all elements from the list. You will see that if there's only one element the — button is disabled.
Note how we used
Add the following code to
Here we are adding an empty wave to the end of the list and with the help of
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.
In
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 }.
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
And now is the time to define our
Here we are building dynamic drop-down menu from the files in Mobs andBosses folders, assigning instances of
If you set up your folder structure right and didn't forget to add
Now all what's left is to add actual wave creation logic to
When a menu item is clicked this method is called with the value object we specified earlier. This value object containing in
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
This article is reproduced, the original address:
http://va.lent.in/unity-make-your-lists-functional-with-reorderablelist/
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 UnityEditorInternalnamespace and looks like this:
Let's see how to code this beautiful interface in Unity IDE usingReorderableList.
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.
Sources: Project sources are on 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.
We will need some test assets: mobs and bosses. We'll be placing them in
Prefabs/Mobsand
Prefabs/Bossesfolders. 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:
Storing data
CreateScriptsfolder where we will store our C# scripts. Create two scripts in this folder:
LevelData.csand
MobWave.cs.
Add this code to
MobWave.cs. This is our value object for storing data for a wave of monsters.
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.
Note: Unity can serialize custom structs since version 4.5.
Add this code to
LevelData.cs. This is just a container for our
MobWaveobjects.
using UnityEngine; using System.Collections.Generic; public class LevelData : MonoBehaviour { public List<MobWave> Waves = new List<MobWave>(); }
Add a
GameObjectto your scene and call it
Data. Add
LevelDatacomponent to this game object and set it up as shown in the GIF animation below.
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.
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 ourLevelData.csscript to change Unity's default list rendering to more functional ReorderableList.
Create
Editorfolder and a new script inside. Call it
LevelDataEditor.cs.
Note: All custom inspectors must be in Editor folder and inherit from
Editorclass.
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:
A custom inspector must inherit from
Editorclass,
To tell Unity that this is an inspector for
LevelDatacomponent we must add
[CustomEditor(typeof(LevelData))]attribute,
private void OnEnable()method is used for initialization,
public override void OnInspectorGUI()method is called when inspector is redrawn.
In our case in
OnEnablewe are creating an instance of
ReorderableListto draw our
Wavesproperty. Don't forget to import UnityEditorInternal namespace:
using UnityEditorInternal;
ReorderableList works with standard C# lists as well as withSerializedProperties. It has two constructors and their more general variants:
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.
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.
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.
Drawing list items
ReorderableList exposes several delegates we can use to customize our lists. The first one isdrawElementCallback. It's called when a list item is drawn.
Add this code in
OnEnableafter the list is created:
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?
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.
Note: I't important to have a semicolon at the end of a lambda };
Here we are getting the list item being drawn:
var element = list.serializedProperty.GetArrayElementAtIndex(index);
We are using
FindPropertyRelativemethod to find properties of the wave:
element.FindPropertyRelative("Type")
And after that we are drawing 3 properties in one line:
Type,
Prefaband
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.
Paste this code in
OnEnablemethod:
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.
Callbacks
Here are all the callbacks exposed by an instance of ReorderableList:drawElementCallback
drawHeaderCallback
onReorderCallback
onSelectCallback
onAddCallback
onAddDropdownCallback
onRemoveCallback
onCanRemoveCallback
onChangedCallback
drawElementCallback
Signature: (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));
drawHeaderCallback
Signature: (Rect rect)
Is used to draw list header.
onReorderCallback
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
onAddCallbackis ignored. As with
onAddCallbackyou 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 addDebug.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
OnEnablemethod:
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
Prefabproperty of selected wave and if it's defined we are calling
EditorGUIUtility.PingObjectmethod 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 usingonCanRemoveCallback.
Add this code to
OnEnablemethod:
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. UsingonRemoveCallbackwe 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.DoRemoveButtonmethod here.
ReorderableList.defaultBehaviourscontains 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 usingonAddCallback.
Add the following code to
OnEnablemethod:
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.FindPropertyRelativemethod we are setting its properties to predefined values. In case of
Prefabproperty 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.
Adding drop-down menu
The last example will be the most interesting. We will make a dynamic drop-down menu activated by + button usingonAddDropdownCallback.
In
Prefabsfolder 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
OnInspectorGUImethod.
private void clickHandler(object target) {}
And now is the time to define our
onAddDropdownCallback. Add this code in
OnEnablemethod:
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 andBosses folders, assigning instances of
WaveCreationParamsto 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
datavariable 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 onlyonChangedCallbackand
onReorderCallbackbecause 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
相关文章推荐
- untiy 3d ShaderLab_第6章_VertexLit渲染路径_4_顶点照明和Unity存放光源的第三种方式
- Unity ScriptableObject的使用
- unity5.3+Easytouch4.3——EasyTouch及摇杆控件介绍
- Unity3D UNET 模仿局域网游戏(三)
- Unity3D Pattern not found 破解失败解决方法
- Unity优化个人体验
- Unity3D UNET 模仿局域网游戏(二)
- Unity3D入门(rolling ball)——学习笔记
- Unity3d+moba+小地图
- Unity3D UNET 模仿局域网游戏(一)
- WWW加载文件
- Unity3D游戏开发之虚拟现实项目开发流程
- Unity+Vuforia 防抖动解决方案
- 关于Unity任何版本点击Play运行就黑屏,除了摄像机窗口其他全部黑掉的问题解决~
- [UnityUI]NGUI性能优化之ScrollView
- 【笔记】Core GameObjects, components, and concepts relating to Unity UI development include
- untiy 3d ShaderLab_第6章_VertexLit渲染路径_3_顶点照明和Unity存放光源的第二种方式
- untiy 3d ShaderLab_第6章_VertexLit渲染路径_2_顶点照明和Unity存放光源的第一种方式
- Unity3D NGUI动态生成模糊背景图
- Unity5.2.3与android通讯