众所周知,在UE4做编辑器扩展是一件无比蛋疼的事情。 首先要考虑是写Plugin还是Module的形式,然后又是加Build.cs,新建文件夹新建文件。涉及到菜单栏扩展还需要知道一堆类的用法,FExtender、FMenuBuilder、FMenuBarBuilder、Command、MenuDelegate等。加菜单,加菜单栏,接口又不一样,AddMenuEntry、AddSubMenu、AddPullDownMenu。 Unity一行代码 [MenuItem]搞定的事情,为什么UE4就这么麻烦呢?
本着我不入地狱谁入地狱的心态,做了一个非常方便扩展菜单栏功能的简易框架(代码不到200行)。最终的效果是这样: 继承特定的类,只要注册好路径,你就可以写你想要的逻辑了。下面会开始讲解这个功能是怎么设计和实现的,如果不感兴趣也可以直接下github把代码复制到自己工程直接用。github工程地址。
EditorModule vs Plugin
首先需要考虑的是菜单栏扩展是写成Plugin还是EditorModule。Plugin意味着代码更独立,方便移植到不同项目里。EditorModule不方便移植,但是可以引用GameModule的类。考虑到编辑器扩展可能会用到GameModule的一些类,比如说将来我们要做一个查找工程里有没有特定类型的蓝图的功能,这个类型包括GameModule任意自定义的类。所以在这个框架里,我选择把菜单栏扩展的功能放在EditorModule。 EditorModule的创建很基础了,网上教程也很多:[Creating an Editor Module]。(https://michaeljcole.github.io/wiki.unrealengine.com/Creating_an_Editor_Module/) 这里就放几张图简单过一下。 文件结构如下:
MenuManager
要想实现最终的效果,我们首先需要划分目标。第一步当然就是扩展最简单的菜单栏。也是网上一堆教程:编辑器扩展:自定义菜单栏。但是鸡佬的做法略有不同,所以这里会讲的细一点。 首先我们新建一个MenuManger继承自EngineSubSystem。 为什么用EngineSubSystem?EngineSubSystem是一种特殊的单例。当Module被加载的时候SubSystem会自动创建并初始化,不需要我们操心它的调用时机。关于菜单栏扩展的核心代码我们将写在MenuManager。
MenuBar、PullDownMenu、SubMenu
接下来创建菜单栏、下拉框、二级菜单、按钮。首先认识MenuBar、PullDownMenu、SubMenu这几个词的区别,以免等下调用相关函数的时候脑子晕掉。 首先在MenuManger的初始化函数加载LevelEditorModule,然后创建FExtender(扩展器类),接下来调用FExtender的AddMenuBarExtension方法。 AddMenuBarExtension有四个参数:
- FName ExtensionHook。要挂在那个菜单附近。
- EExtensionHook::Position HookPosition。要挂的位置的类型,前或后,还有另一个类型用不着。
- const TSharedPtr< FUICommandList >& CommandList。可以用于绑定通用的按钮操作,比如复制粘贴撤销等。这个案例里我们用不着,可以直接使用nullptr。可以参考CurveEditor类对CommandList的使用。
- const FMenuBarExtensionDelegate& MenuBarExtensionDelegate。菜单栏扩展委托。使用FMenuBarExtensionDelegate::CreateXXX型函数进行创建委托。除了CreateUObject还有CreateStatic、CreateLambda等其他方式。注意这里的CreateUObject不是创建UObject的意思,而是CreateByUObjet,即通过提供的这个UObject类的这个方法来创建一个委托。
回到代码,AddMenuBarExtension那行的意思就是告诉扩展器我要在Help的后面插入MenuBar,不绑定通用操作,要插入的菜单长什么样,叫什么名字,由我MenuManager的AddMenuBarExtension告诉你。
然后是MenuMager的AddMenuBarExtension及相关代码:
- AddMenuBarExtension调用AddPullDownMenu告诉编辑器要在“First”的菜单栏里添加下拉框。注意使用的是FMenuBarBuilder参数
- AddMenuExtension调用AddSubMenu告诉编辑器要在“Second”的下拉框中添加子菜单。使用FMenuBuilder参数。
- AddSubMenuExtension调用AddMenuEntry告诉编辑器要在“Test”的菜单中注册方法来调用。使用FMenuBuilder参数。注册委托使用的FUIAction类。
- 最终调用LogTestFunc方法。
最后写完就是出现这样的菜单栏,点击以后会打log。
Class Default Object
大部分网上的教程也就到上面那一步了。但是,离真正可以投入实际项目使用还差的很远。下面鸡佬将讲解本框架最精华的部分。 上面讲到,如果要绑定委托,有好几种方法。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<FMenuItemNode>& 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<FString> 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里写逻辑即可。
学习资料
关于作者
- 水曜日鸡,喜欢ACG的游戏程序员。曾参与索尼中国之星项目《硬核机甲》的开发。 目前在某大厂做UE4项目。
CSDN博客:https://blog.csdn.net/j756915370 知乎专栏:https://zhuanlan.zhihu.com/c_1241442143220363264 游戏同行聊天群:891809847
|