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

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

本着我不入地狱谁入地狱的心态,做了一个非常方便扩展菜单栏功能的简易框架(代码不到200行)。最终的效果是这样:
在这里插入图片描述
在这里插入图片描述
继承特定的类,只要注册好路径,你就可以写你想要的逻辑了。下面会开始讲解这个功能是怎么设计和实现的,如果不感兴趣也可以直接下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/)
这里就放几张图简单过一下。
在这里插入图片描述
在这里插入图片描述
文件结构如下:
在这里插入图片描述


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,首先是在初始化函数里构造树。
在这里插入图片描述
重新编写AddMenuBarExtensionAddMenuExtension方法。对于根节点,调用AddPullDownMenu,对于根节点以外的节点则需要判断是叶子节点还是父节点。是叶子节点则调用MenuEntry注册委托。
在这里插入图片描述
这样就大功告成了,以后想注册新的菜单栏,只需要继承自UMenuItem,调用Init函数注册路径,然后在OnMenuClick里写逻辑即可。
在这里插入图片描述


学习资料


关于作者

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

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

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值