Unity自身提供的编辑功能是很强大,但也是受限的。在构建游戏的过程中,如果我们只能依赖于Unity自身的对象编辑功能,在很多地方将无法满足开发效率和工作流的需要,例如如下的场景:

  1. 制作一个有对话系统的RPG游戏,想要把对话的选择条件、编辑和跳转可视化;
  2. 想用行为树来建模怪物AI,并且需要一个编辑行为树节点的可视化编辑器;
  3. 在制作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 类相似的回调,例如 OnEnableAwakeOnDisable。不过最重要的一个回调应该是 OnInspectorGUI ,我们需要使用这个回调来进行绝大部分的动作,例如GUI绘制和控制消息处理。

Unity Doc地址

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?

Unity Doc地址

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, EditorGUIEditorGUILayout

其中,以 Layout 结尾的两个类会自动对GUI内容进行排版,并且可以混合使用。它们提供的接口和 GUIEditorGUI 类分别基本一致,只是无Layout版本需要额外的提供控件的绘制范围参数。

GUI 类提供的是一些比较基本的控件,包括文本框(TextAreaTextField),复选框(Toggle),按钮(Button)等。它还可以创建子区域(BeginGroup),子窗口(Window),滚动区域(BeginScrollView)等,并在其中进行进一步的GUI处理。除了比较高层的控件绘制,GUI类也有很底层的绘制方法,例如 GUI.DrawTexture,可以将一个贴图直接绘制到一个矩形区域中。

EditorGUI 类的工作原理相同,但提供的是一些编辑器相关的控件,例如 PropertyFieldMinMaxSliderColorField 等。在进行编辑器的内容安排时通常更多的会用到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.BeginWindowsEditorWindow.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。