【UE·Editor篇】做一个超好用的菜单栏扩展框架

众所周知,在UE4做编辑器扩展是一件无比蛋疼的事情
首先要考虑是写Plugin还是Module的形式,然后又是加Build.cs,新建文件夹新建文件。涉及到菜单栏扩展还需要知道一堆类的用法,FExtender、FMenuBuilder、FMenuBarBuilder、Command、MenuDelegate等。加菜单,加菜单栏,接口又不一样,AddMenuEntry、AddSubMenu、AddPullDownMenu。
Unity一行代码 [MenuItem]搞定的事情,为什么UE4就这么麻烦呢?
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第1张图片

本着我不入地狱谁入地狱的心态,做了一个非常方便扩展菜单栏功能的简易框架(代码不到200行)。最终的效果是这样:
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第2张图片
在这里插入图片描述
继承特定的类,只要注册好路径,你就可以写你想要的逻辑了。下面会开始讲解这个功能是怎么设计和实现的,如果不感兴趣也可以直接下github把代码复制到自己工程直接用。github工程地址。


EditorModule vs Plugin

首先需要考虑的是菜单栏扩展是写成Plugin还是EditorModulePlugin意味着代码更独立,方便移植到不同项目里EditorModule不方便移植,但是可以引用GameModule的类。考虑到编辑器扩展可能会用到GameModule的一些类,比如说将来我们要做一个查找工程里有没有特定类型的蓝图的功能,这个类型包括GameModule任意自定义的类。所以在这个框架里,我选择把菜单栏扩展的功能放在EditorModule。
EditorModule的创建很基础了,网上教程也很多:[Creating an Editor Module]。(https://michaeljcole.github.io/wiki.unrealengine.com/Creating_an_Editor_Module/)
这里就放几张图简单过一下。
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第3张图片
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第4张图片
文件结构如下:
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第5张图片


MenuManager

要想实现最终的效果,我们首先需要划分目标。第一步当然就是扩展最简单的菜单栏。也是网上一堆教程:编辑器扩展:自定义菜单栏。但是鸡佬的做法略有不同,所以这里会讲的细一点。
首先我们新建一个MenuManger继承自EngineSubSystem
为什么用EngineSubSystem?EngineSubSystem是一种特殊的单例。当Module被加载的时候SubSystem会自动创建并初始化,不需要我们操心它的调用时机。关于菜单栏扩展的核心代码我们将写在MenuManager。
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第6张图片


MenuBar、PullDownMenu、SubMenu

接下来创建菜单栏、下拉框、二级菜单、按钮。首先认识MenuBar、PullDownMenu、SubMenu这几个词的区别,以免等下调用相关函数的时候脑子晕掉。
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第7张图片
首先在MenuManger的初始化函数加载LevelEditorModule,然后创建FExtender(扩展器类),接下来调用FExtender的AddMenuBarExtension方法。
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第8张图片
AddMenuBarExtension有四个参数:

  • FName ExtensionHook。要挂在那个菜单附近。
  • EExtensionHook::Position HookPosition。要挂的位置的类型,前或后,还有另一个类型用不着。
  • const TSharedPtr< FUICommandList >& CommandList。可以用于绑定通用的按钮操作,比如复制粘贴撤销等。这个案例里我们用不着,可以直接使用nullptr。可以参考CurveEditor类对CommandList的使用。【UE·Editor篇】做一个超好用的菜单栏扩展框架_第9张图片
  • const FMenuBarExtensionDelegate& MenuBarExtensionDelegate。菜单栏扩展委托。使用FMenuBarExtensionDelegate::CreateXXX型函数进行创建委托。除了CreateUObject还有CreateStatic、CreateLambda等其他方式。注意这里的CreateUObject不是创建UObject的意思,而是CreateByUObjet,即通过提供的这个UObject类的这个方法来创建一个委托。

回到代码,AddMenuBarExtension那行的意思就是告诉扩展器我要在Help的后面插入MenuBar,不绑定通用操作,要插入的菜单长什么样,叫什么名字,由我MenuManager的AddMenuBarExtension告诉你

然后是MenuMager的AddMenuBarExtension及相关代码:
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第10张图片

  • 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,我们就可以在不需要管理类创建的情况下,将类的方法绑定到菜单上
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第11张图片
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第12张图片
看起来好像和Static来绑定也没啥本质区别的?别急,如果我说可以查找到所有需要绑定委托的类,并且统一进行绑定呢?
TObjectIterator对象迭代器,可以查找所有对象。把T代入UClass就可以查找所有UClass。然后我们把所有需要绑定的类都继承一个基类,最后用迭代器遍历的时候对UClass进行判断就可以找到所有需要绑定委托的类了。
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第13张图片
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第14张图片
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第15张图片


设计数据结构

找UClass也有了,绑定方法也实现了。现在需要的是用合适的数据结构把这些类统筹起来。并且需要对指定路径进行解析。比如说菜单路径是“First/Second/Third/Button”,另一个是“A/B/C/D”。怎么让“First"和"A"在调用AddPullDownMenu的时填充进去,“Button”和“D”在调用AddMenuEntry的时候填充进去,其他中间的在调用AddSubMenu的时候填充进去。
很显然这是一个树形结构加上一个数组结构【UE·Editor篇】做一个超好用的菜单栏扩展框架_第16张图片
对于“First”和“A”这种要添加到菜单栏的字符串来说,它们应该是树的根节点。然后再用一个数组把所有根节点连接起来。节点类MenuItemNode结构如下:
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第17张图片
然后在MenuManger添加FMenuItemNode的数组记录根节点。添加构造树的方法。
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第18张图片
接着在MenuItem添加路径、菜单名称、菜单提示、初始化函数。
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第19张图片
然后是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,首先是在初始化函数里构造树。
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第20张图片
重新编写AddMenuBarExtensionAddMenuExtension方法。对于根节点,调用AddPullDownMenu,对于根节点以外的节点则需要判断是叶子节点还是父节点。是叶子节点则调用MenuEntry注册委托。
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第21张图片
这样就大功告成了,以后想注册新的菜单栏,只需要继承自UMenuItem,调用Init函数注册路径,然后在OnMenuClick里写逻辑即可。
【UE·Editor篇】做一个超好用的菜单栏扩展框架_第22张图片


学习资料

  • 一文搞懂StaticClass、GetClass和ClassDefaultObject
  • 编辑器扩展:自定义菜单栏
  • Creating an Editor Module
  • UE4 插件扩展引擎工具栏
  • UE4 编辑器扩展 优雅地扩展菜单栏
  • UE4.26 几种编辑器扩展方法
  • 高级编辑器扩展合集(自定义布局、自定义Filter、自定义预览窗口等)
  • 【UE4】编辑器开发(一)关卡编辑器拓展
  • 【UE4】编辑器开发(二)ContentBrowser拓展
  • 【UE4】编辑器开发(三)资源类型拓展
  • 【UE4】编辑器开发(四)属性面板(Details)拓展
  • UE体验优化(1)-----使用MetaValue降低Editor扩展复杂度

关于作者

  • 水曜日鸡,喜欢ACG的游戏程序员。曾参与索尼中国之星项目《硬核机甲》的开发。 目前在某大厂做UE4项目。

CSDN博客:https://blog.csdn.net/j756915370
知乎专栏:https://zhuanlan.zhihu.com/c_1241442143220363264
游戏同行聊天群:891809847

你可能感兴趣的:(UE4,编辑器,ue4,游戏开发,c++)