众所周知,在UE4做编辑器扩展是一件无比蛋疼的事情。
首先要考虑是写Plugin还是Module的形式,然后又是加Build.cs,新建文件夹新建文件。涉及到菜单栏扩展还需要知道一堆类的用法,FExtender、FMenuBuilder、FMenuBarBuilder、Command、MenuDelegate等。加菜单,加菜单栏,接口又不一样,AddMenuEntry、AddSubMenu、AddPullDownMenu。
Unity一行代码 [MenuItem]搞定的事情,为什么UE4就这么麻烦呢?
本着我不入地狱谁入地狱的心态,做了一个非常方便扩展菜单栏功能的简易框架(代码不到200行)。最终的效果是这样:
继承特定的类,只要注册好路径,你就可以写你想要的逻辑了。下面会开始讲解这个功能是怎么设计和实现的,如果不感兴趣也可以直接下github把代码复制到自己工程直接用。github工程地址。
首先需要考虑的是菜单栏扩展是写成Plugin还是EditorModule。Plugin意味着代码更独立,方便移植到不同项目里。EditorModule不方便移植,但是可以引用GameModule的类。考虑到编辑器扩展可能会用到GameModule的一些类,比如说将来我们要做一个查找工程里有没有特定类型的蓝图的功能,这个类型包括GameModule任意自定义的类。所以在这个框架里,我选择把菜单栏扩展的功能放在EditorModule。
EditorModule的创建很基础了,网上教程也很多:[Creating an Editor Module]。(https://michaeljcole.github.io/wiki.unrealengine.com/Creating_an_Editor_Module/)
这里就放几张图简单过一下。
文件结构如下:
要想实现最终的效果,我们首先需要划分目标。第一步当然就是扩展最简单的菜单栏。也是网上一堆教程:编辑器扩展:自定义菜单栏。但是鸡佬的做法略有不同,所以这里会讲的细一点。
首先我们新建一个MenuManger继承自EngineSubSystem。
为什么用EngineSubSystem?EngineSubSystem是一种特殊的单例。当Module被加载的时候SubSystem会自动创建并初始化,不需要我们操心它的调用时机。关于菜单栏扩展的核心代码我们将写在MenuManager。
接下来创建菜单栏、下拉框、二级菜单、按钮。首先认识MenuBar、PullDownMenu、SubMenu这几个词的区别,以免等下调用相关函数的时候脑子晕掉。
首先在MenuManger的初始化函数加载LevelEditorModule,然后创建FExtender(扩展器类),接下来调用FExtender的AddMenuBarExtension方法。
AddMenuBarExtension有四个参数:
回到代码,AddMenuBarExtension那行的意思就是告诉扩展器我要在Help的后面插入MenuBar,不绑定通用操作,要插入的菜单长什么样,叫什么名字,由我MenuManager的AddMenuBarExtension告诉你。
然后是MenuMager的AddMenuBarExtension及相关代码:
大部分网上的教程也就到上面那一步了。但是,离真正可以投入实际项目使用还差的很远。下面鸡佬将讲解本框架最精华的部分。
上面讲到,如果要绑定委托,有好几种方法。Static?但是把所有要扩展菜单的方法都写成Static既麻烦又不能实现自动化。UObject?但是需要对象啊,谁来创建对象呢?创建了以后是不是还要统一管理也麻烦啊。不对,UObject真的需要我们创建对象吗?我们是不是忘了还有CDO的存在。
CDO,ClassDefaultObject,所有UObject类都存在的一个默认对象。关于CDO,我以前也讲过这里不展开了:【UE·底层篇】一文搞懂StaticClass、GetClass和ClassDefaultObject。
有了CDO,我们就可以在不需要管理类创建的情况下,将类的方法绑定到菜单上:
看起来好像和Static来绑定也没啥本质区别的?别急,如果我说可以查找到所有需要绑定委托的类,并且统一进行绑定呢?
TObjectIterator对象迭代器,可以查找所有对象。把T代入UClass就可以查找所有UClass。然后我们把所有需要绑定的类都继承一个基类,最后用迭代器遍历的时候对UClass进行判断就可以找到所有需要绑定委托的类了。
找UClass也有了,绑定方法也实现了。现在需要的是用合适的数据结构把这些类统筹起来。并且需要对指定路径进行解析。比如说菜单路径是“First/Second/Third/Button”,另一个是“A/B/C/D”。怎么让“First"和"A"在调用AddPullDownMenu的时填充进去,“Button”和“D”在调用AddMenuEntry的时候填充进去,其他中间的在调用AddSubMenu的时候填充进去。
很显然这是一个树形结构加上一个数组结构。
对于“First”和“A”这种要添加到菜单栏的字符串来说,它们应该是树的根节点。然后再用一个数组把所有根节点连接起来。节点类MenuItemNode结构如下:
然后在MenuManger添加FMenuItemNode的数组记录根节点。添加构造树的方法。
接着在MenuItem添加路径、菜单名称、菜单提示、初始化函数。
然后是AddMenuItemToNodeList的实现:
//查找节点
static FMenuItemNode* FindMenuItemNode(TArray& MenuNodes, const FString& MenuName)
{
for (FMenuItemNode& Node : MenuNodes)
{
if (Node.NodeName == MenuName)
{
return &Node;
}
}
return nullptr;
}
void UMenuManager::AddMenuItemToNodeList(UMenuItem* MenuItem)
{
if (MenuItem == nullptr)
{
return;
}
//将路径ABCD分解,按顺序存储数组
TArray MenuNames;
FString Path = MenuItem->GetMenuPath();
if (Path.IsEmpty())
{
return;
}
FString Left;
while (Path.Split("/", &Left, &Path))
{
if (Left.IsEmpty())
{
continue;
}
MenuNames.Add(Left);
}
MenuNames.Add(Path);
//查找根节点,没有则创建
FMenuItemNode* RootMenuNode = FindMenuItemNode(RootNodeList, MenuNames[0]);
if (RootMenuNode == nullptr)
{
FMenuItemNode MenuItemNode;
MenuItemNode.NodeName = MenuNames[0];
int32 Index = RootNodeList.Add(MenuItemNode);
RootMenuNode = &RootNodeList[Index];
}
//根据上面记录的字符串数组循环查找,没有则创建节点
FMenuItemNode* ParentNode = RootMenuNode;
for (int i = 1; i < MenuNames.Num(); ++i)
{
FString& ChildName = MenuNames[i];
FMenuItemNode* ChildNode = FindMenuItemNode(ParentNode->Children, ChildName);
if (ChildNode == nullptr)
{
FMenuItemNode MenuItemNode;
MenuItemNode.NodeName = ChildName;
int32 Index = ParentNode->Children.Add(MenuItemNode);
ChildNode = &ParentNode->Children[Index];
}
ParentNode = ChildNode;
}
//最后给叶子节点赋值MenuItem的指针
ParentNode->MenuItem = MenuItem;
}
最后回到MenuManger,首先是在初始化函数里构造树。
重新编写AddMenuBarExtension和AddMenuExtension方法。对于根节点,调用AddPullDownMenu,对于根节点以外的节点则需要判断是叶子节点还是父节点。是叶子节点则调用MenuEntry注册委托。
这样就大功告成了,以后想注册新的菜单栏,只需要继承自UMenuItem,调用Init函数注册路径,然后在OnMenuClick里写逻辑即可。
关于作者
CSDN博客:https://blog.csdn.net/j756915370
知乎专栏:https://zhuanlan.zhihu.com/c_1241442143220363264
游戏同行聊天群:891809847