Godot自定义菜单构建
在Godot中,编辑器扩展和在Unity中相比底层的支持更少,从引擎的量级对比上就可以看出。默认的可添加UI控件的位置并不包含左上角和场景、项目等并列的位置,这就导致使用起来不方便的问题。
另一个问题是,当使用外部编辑器之后,对于EditorScript的运行便无法通过常规的Script->文件->执行调用,这样导致如果要频繁地调用简单的编辑器专用脚本,切换时非常麻烦。因此需要采用EditorPlugin的方式来创建,但这样就回到了第一个问题。
考虑到Godot使用的是构建游戏的所有对象来构建自己的引擎,二者是同源的,Godot的UI也是在启动的时候构建成了一个树状结构,那么我们可以筛查这个UI结构,找到要插入控件的父对象,然后把我们要添加的自定义菜单PopupMenu对象作为孩子插入即可。
定位语法树
既然语法树是一个树状结构,我们可以通过递归调用的方式获取并输出每一个节点。获取根节点的接口是EditorInterface.Singleton.GetBaseControl(),通过依次递归调用,得到了从根节点到目标位置的语法树(Godot版本4.4.1 NET.,不保证稳定):
- @Panel@14 (Panel)
- @VBoxContainer@15 (VBoxContainer)
- @EditorTitleBar@16 (HBoxContainer)
- @MenuBar@103 (MenuBar)
- 场景 (PopupMenu)
- @PopupMenu@4924 (PopupMenu)
- @PopupMenu@4930 (PopupMenu)
- 项目 (PopupMenu)
- @PopupMenu@4941 (PopupMenu)
- CSharpTools (PopupMenu)
- @PopupMenu@14113 (PopupMenu)
- @PopupMenu@4941 (PopupMenu)
- 调试 (PopupMenu)
- 编辑器 (PopupMenu)
- @PopupMenu@4954 (PopupMenu)
- @PopupMenu@4960 (PopupMenu)
- 帮助 (PopupMenu)
- 场景 (PopupMenu)
- @MenuBar@103 (MenuBar)
- @EditorTitleBar@16 (HBoxContainer)
- @VBoxContainer@15 (VBoxContainer)
根据Godot的API:
使用@EditorTitleBar@*这样的方式可以找到最终的位置,此时调用AddChild方法添加一个PopedMenu对象即可。
添加可执行项
现在离成功执行脚本只差一步,只有向PopupMenu中添加元素才能有效果。这一点很简单,调用AddItem接口,传递选项显示内容和唯一ID即可做到。
关于处理菜单选择,PopupMenu通过IdPressed信号来接收操作,该信号传递一个类型为long的id,表示对应的选项被按下,这个时候需要自己处理该怎么操作,比如维护一个回调Dictionary。信号处理函数如下:
private Dictionary<long, Action> _menuCallbacks = new();
private void OnMenuItemPressed(long id)
{
if (_menuCallbacks.TryGetValue(id, out Action action))
action?.Invoke();
}
热重载
使用C#比较麻烦的一点是,任何改动如果要生效都需要重新Build,这就涉及到一个热重载的问题。我做了一个简易的实验,在构造函数和 _ExitTree中提供字段的输出,在 _EnterTree中进行字段的设置,得到了以下的结果:
其中True和False输出的是是否为空。
根据实验可以得到以下结论:
- 当EditorPlugin在启用的情况下Build,那么这个对象将会被销毁并重新构建,同时会复制之前所有的字段值,如果是引用对象也会复制原来的引用。
- Build的过程中不会触发 _EnterTree函数和 _ExitTree函数,但是会调用构造函数。
- Build的过程中不应该预设UI字段仍然有效,所以在_ExitTree函数中应该重新FindChild。
妥协
因此,鉴于引用是直接复制的,那么要求就是向管理目录的对象中添加的回调一定是static的,即不包含实际调用的信息,不包含对象的成员。以下面的代码为例,ScanDeployAsset是一个static函数,具体内容省略。
public partial class DeployScanner : EditorPlugin
{
private static readonly string itemName = "扫描可部署资产";
public override void _EnterTree()
{
GetTree().ProcessFrame += Register;
}
public override void _ExitTree()
{
ProjectTopMenu.Instance?.RemoveItem(itemName);
}
public void Register()
{
GetTree().ProcessFrame -= Register;
ProjectTopMenu.Instance?.AddItem(itemName, ScanDeployAsset);
}
}