3. EditorWindow基础:创建编辑器窗口、GUI布局、窗口生命周期、Repaint机制
好,咱们今天来聊聊 EditorWindow。这是 Unity 编辑器扩展里最基础、也最核心的一块。
说白了,你做的所有工具,最终都要有个界面让用户操作。这个界面,就是 EditorWindow。
我记得我刚入行那会儿,第一次写编辑器工具,就是照着官方文档抄了一个窗口出来。当时觉得挺神奇的——原来 Unity 里还能自己开个窗口?后来做多了才发现,这里面的门道还真不少。
3.1 创建你的第一个编辑器窗口
创建一个窗口,其实就两步:继承 EditorWindow,然后用 GetWindow 把它打开。
using UnityEditor;
using UnityEngine;
public class MyFirstWindow : EditorWindow
{
[MenuItem("Tools/我的第一个窗口")]
public static void ShowWindow()
{
// 获取或创建窗口实例
MyFirstWindow window = GetWindow<MyFirstWindow>();
window.titleContent = new GUIContent("我的窗口");
window.Show();
}
}
这段代码,你直接扔到 Editor 文件夹下,编译完就能在菜单栏看到「Tools/我的第一个窗口」了。
点击一下,窗口就出来了。
我个人习惯把窗口的创建方法写成静态的,然后用 MenuItem 挂上去。这样既可以从菜单打开,也可以在其他脚本里通过代码调用。
3.2 GUI布局:从混乱到有序
窗口创建好了,接下来就是往里面塞东西。Unity 编辑器窗口的 GUI 绘制,用的是 OnGUI 方法。
private void OnGUI()
{
GUILayout.Label("这是一个标签", EditorStyles.boldLabel);
if (GUILayout.Button("点击我"))
{
Debug.Log("按钮被点击了");
}
// 输入框
_myString = EditorGUILayout.TextField("输入内容", _myString);
}
这里要注意,OnGUI 每帧都会被调用。你写的布局代码,其实是在描述「每一帧怎么画」。所以不要在 OnGUI 里做耗时操作,也不要在里面 new 对象。
布局方式有两种:
- 自动布局(GUILayout / EditorGUILayout):你只管往里塞控件,Unity 自动帮你排位置。适合快速开发。
- 固定布局(GUI / EditorGUI):你得自己算 Rect,精确控制每个控件的位置和大小。适合做复杂界面。
我在项目中遇到过一个问题:用自动布局做了一堆折叠面板,结果窗口一多,性能就崩了。后来发现是每帧都在重新计算布局。换成固定布局后,性能好了很多。
3.3 窗口生命周期:从打开到关闭
一个 EditorWindow 的生命周期,其实比你想的要复杂一些。
| 阶段 | 触发时机 | 说明 |
|---|---|---|
| Awake | 窗口被创建时 | 只调用一次。适合初始化一些静态数据。 |
| OnEnable | 窗口被激活时 | 每次窗口显示都会调用。包括反序列化后。 |
| OnFocus | 窗口获得焦点时 | 用户点击了你的窗口。 |
| OnGUI | 每帧绘制时 | 核心方法,所有界面绘制都在这里。 |
| OnLostFocus | 窗口失去焦点时 | 用户点了别的地方。 |
| OnDisable | 窗口被隐藏时 | 关闭窗口或切换布局时触发。 |
| OnDestroy | 窗口被销毁时 | 彻底移除窗口。 |
你想想看,如果你在 OnEnable 里订阅了某个事件,那在 OnDisable 里一定要取消订阅。不然窗口关了,事件还在,就会报错。
嗯,这里要注意:Unity 编辑器窗口的序列化机制和运行时 MonoBehaviour 不太一样。EditorWindow 继承自 ScriptableObject,所以它的序列化是靠 Unity 的序列化系统来做的。
private void OnEnable()
{
// 反序列化时,恢复数据
if (_myData == null)
{
_myData = new MyData();
}
// 订阅事件
EditorApplication.playModeStateChanged += OnPlayModeChanged;
}
private void OnDisable()
{
// 取消订阅,防止内存泄漏
EditorApplication.playModeStateChanged -= OnPlayModeChanged;
}
3.4 Repaint机制:别让你的窗口卡死
Repaint,说白了就是「重绘」。Unity 编辑器窗口不会自动刷新,你得告诉它「嘿,该重画了」。
什么时候需要手动 Repaint?
- 数据变了,界面需要更新
- 有动画效果
- 响应外部事件
private void Update()
{
// 每帧都重绘,适合做动画
Repaint();
}
private void OnInspectorUpdate()
{
// 当 Inspector 更新时触发,频率比 Update 低
// 适合做一些后台检查
if (_needRepaint)
{
Repaint();
_needRepaint = false;
}
}
我曾经做过一个资源浏览器工具,里面有个缩略图预览。一开始我每帧都 Repaint,结果打开 10 个窗口,编辑器直接卡成 PPT。后来改成只在资源变化时才 Repaint,性能瞬间就上来了。
还有一个技巧:用 EditorApplication.update 来做定时刷新。
private void OnEnable()
{
EditorApplication.update += OnEditorUpdate;
}
private void OnDisable()
{
EditorApplication.update -= OnEditorUpdate;
}
private void OnEditorUpdate()
{
// 每帧调用,但比 OnGUI 轻量
// 适合做数据检查
if (条件满足)
{
Repaint();
}
}
你想想看,Repaint 其实就是一个信号。你发出信号,Unity 在下一帧就会重新调用 OnGUI。所以不要在 OnGUI 里修改数据,不然会陷入「修改-重绘-再修改-再重绘」的死循环。
好了,关于 EditorWindow 的基础,咱们就聊这么多。记住三个核心点:
- 用 GetWindow 创建窗口,用 OnGUI 绘制界面
- 生命周期方法要配对使用,特别是事件订阅
- Repaint 要节制,只在需要的时候调用
下一章,咱们会深入讲讲如何用 EditorGUI 和 EditorGUILayout 做更复杂的界面。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。