Unity自身提供的编辑功能是很强大,但也是受限的。在构建游戏的过程中,如果我们只能依赖于Unity自身的对象编辑功能,在很多地方将无法满足开发效率和工作流的需要,例如如下的场景:
- 制作一个有对话系统的RPG游戏,想要把对话的选择条件、编辑和跳转可视化;
- 想用行为树来建模怪物AI,并且需要一个编辑行为树节点的可视化编辑器;
- 在制作2D tile-based(基于网格)的游戏时,需要一个能对齐到网格的地图编辑器;
- …
这些都对编辑器的功能提供了新层次的要求:简单修改对象内部的值并不够,还需要提供一种能自定义的、有意义的编辑方式。
所幸Unity在自定义编辑器方面,提供的支持相当齐全。我最近就通过Unity的自定义编辑器接口,实现了一个2D基于网格的地图编辑器,也想借此机会把学到的知识整理一下。
这篇文章是这一系列总结文章的第一篇,主要对Unity提供的扩展窗口的接口和绘制窗口的方式进行介绍。
1. 创建窗口
在Unity中允许对编辑器进行的扩展主要分为两种:通过继承 Editor
类来扩展Inspector窗口,和继承 EditorWindow
类来创建自定义显示窗口.
1.1. Editor Scipt
所有编辑器相关的代码是不会出现在最终游戏中的,因此Unity用一个特殊的规则来区分这些脚本:任何放置在 Editor
文件夹(不一定是根目录下,任何子目录的Editor文件夹都可以)
下的脚本都被认为是编辑器脚本——它们会被Unity读取和使用,并且不能被非Editor脚本的普通脚本引用。因此,要记得把所有编辑器相关的代码都放到Editor文件夹中。
1.2. Editor
Editor
类允许自定义特定类型的 Inspector(检视器)窗口,也就是我们编辑对象值的时候常用的那个窗口。
我们需要在自定义的Editor类型前加上 [CustomEditor(System.Type)]
属性,来指明它对应的是哪个类型。如果这个编辑器需要多对象编辑的功能,还可以加上 [CanEditMultipleObjects]
属性。
Editor有一些和 MonoBehaviour
类相似的回调,例如 OnEnable
,Awake
和 OnDisable
。不过最重要的一个回调应该是 OnInspectorGUI
,我们需要使用这个回调来进行绝大部分的动作,例如GUI绘制和控制消息处理。
1.3. EditorWindow
EditorWindow
则代表一个独立的编辑器窗口。Unity不会自动调用它,而是需要我们去主动打开。例如,下面的代码创建一个窗口,显示“PHP是世界上最好的语言”,并且可以从菜单项 Custom/PHP
打开它。
using UnityEngine;
using UnityEditor;
using System.Collections;
public class PHPWindow : EditorWindow {
[MenuItem("Custom/PHP")]
public static void Open() {
var window = new PHPWindow();
window.name = "PHP Editor";
window.Show();
}
void OnGUI() {
GUILayout.Label("PHP is the best language in the world");
}
}
和 Editor
不同,EditorWindow
处理GUI事件的回调方法是 OnGUI
。
这个窗口可以和任何其他的编辑器子窗口一样进行拖动、缩放和停靠。Neat isn’t it?
2. GUI绘制和处理
无论是Editor还是EditorWindow,我们最终都需要在回调方法中进行GUI绘制和事件处理。Unity提供了一套统一的 Immediate-mode(立即模式)接口来进行编辑器GUI的处理,接下来我们对它进行简单的介绍。
2.1. 立即模式 (Immediate Mode)
Unity的编辑器部分使用的是“立即模式”GUI。通常的GUI实现方式,一般每个控件都是一个独立的类,有它独立的位置/样式/回调存储。而立即模式的GUI则是stateless(无状态)的,每个控件实际上就是GUI回调中的一次方法调用,例如:
string path = "";
void OnGUI() {
path = GUILayout.TextField(path);
if (path != "") {
if (GUILayout.Button("Create")) {
// Do button callback...
}
}
}
这样的GUI绘制方法对实时性、动态性要求高的程序来说是很方便的,因为我们可以很容易控制控件的产生和消失。GUI的数据直接由使用者管理,而GUI控件的回调则返回对数据的更改。
可以看看这篇文档来了解IMGUI的更多信息。
2.2. GUI接口
Unity的GUI绘制部分由四个类接管:GUI
, GUILayout
, EditorGUI
和 EditorGUILayout
。
其中,以 Layout
结尾的两个类会自动对GUI内容进行排版,并且可以混合使用。它们提供的接口和 GUI
、EditorGUI
类分别基本一致,只是无Layout版本需要额外的提供控件的绘制范围参数。
GUI
类提供的是一些比较基本的控件,包括文本框(TextArea
,TextField
),复选框(Toggle
),按钮(Button
)等。它还可以创建子区域(BeginGroup
),子窗口(Window
),滚动区域(BeginScrollView
)等,并在其中进行进一步的GUI处理。除了比较高层的控件绘制,GUI类也有很底层的绘制方法,例如 GUI.DrawTexture
,可以将一个贴图直接绘制到一个矩形区域中。
EditorGUI
类的工作原理相同,但提供的是一些编辑器相关的控件,例如 PropertyField
,MinMaxSlider
,ColorField
等。在进行编辑器的内容安排时通常更多的会用到EditorGUI。使用EditorGUI,我们可以做到Unity对象编辑能实现的所有功能。
自动Layout版本的IMGUI有一个特别好的feature——可以很方便的让一个编辑器的内容“嵌入”一个子窗口或者一个子区域。例如,假如你想在一个编辑器的子区域中放一个对象编辑器,只需要类似下面的调用就可以了:
void OnGUI() {
GUI.BeginGroup(...);
var editor = Editor.CreateEditor(...);
editor.OnInspectorGUI();
GUI.EndGroup();
}
2.3. 事件处理
Event.current
对应了GUI回调的当前事件。它可以是简单的重绘,也可以是鼠标移动、按键按下等代表输入处理的事件。
Event是对当前GUI的上下文敏感的。例如,如果你在GUI中创建了一个子窗口,在这个子窗口中拿到的 Event.current
中的鼠标位置就是相对于当前窗口top-left corner的。
3. Sidenote: 吐槽和坑
1:EditorWindow使用 GUI.Window
回调创建子窗口的时候,必须把调用包含在 EditorWindow.BeginWindows
和 EditorWindow.EndWindows
之中。然而这两个函数只隐藏在文档 EditorWindow 的函数列表之中,找半天才发现,刚刚开始的时候还以为子窗口在这里根本用不了,吐血。
2:Unity的官方文档在这部分的cross-reference让人在摸清结构的时候很头痛啊T^T 就说下个人的体验:首先从Editor开始,发现OnInspectorGUI可以处理回调 -> 不知道哪里可以进行GUI绘制 -> (狂搜)找到 EditorGUILayout -> 发现 EditorGUILayout 无法自定义位置 -> (狂搜)找到EditorGUI -> 然后又找到了GUI和GUILayout,然后文档里又没有说清楚这四个类的相互关系,在说EditorGUI的时候只有这么一句:These work pretty much like the normal GUI functions - and also have matching implementations in EditorGUILayout
,并没有说明它们的区别。要是能在文档里加入一些比较重要的cross-reference会好很多。
这篇文章主要整理了Unity的编辑器创建和GUI绘制的一些接口。在下一篇文章里,会谈一谈编辑器实际应用中的比较重要的一个问题:创建和读取自定义asset。