UE4插件与一些编辑器扩展总结
前言:
.uplugin与.uproject
前面的版本号、版本名、插件名等在编辑器下创建插件就会有对应生成。
值得一提的是"module"与"Plugins":
![](https://pic2.zhimg.com/80/v2-5382f380a8608a22ff6f24a3a53b9f15_1440w.jpg)
比如我做的UCharts插件,这里头Type可填写的值范围:
(此处参考【UE4】插件与模块 - 知乎 (zhihu.com)
namespace EHostType
{
enum Type
{
Runtime, //运行时,任何情况下
RuntimeNoCommandlet,
RuntimeAndProgram,
CookedOnly,
Developer, //开发时使用的插件
Editor, //编辑器类型插件
EditorNoCommandlet,
Program, //只有运行独立程序时的插件
ServerOnly,
ClientOnly,
Max
};
}
LoadingPhase的值范围:
namespace ELoadingPhase
{
enum Type
{
PostConfigInit, //引擎完全加载前,配置文件加载后。适用于较底层的模块。
PreEarlyLoadingScreen, //在UObject加载前,用于补丁系统
PreLoadingScreen, //在引擎模块完全加载和加载页面之前
PreDefault, //默认模块加载之前阶段
Default, //默认加载阶段,在引擎初始化时,游戏模块加载之后
PostDefault, //默认加载阶段之后加载
PostEngineInit, //引擎初始化后
None, //不自动加载模块
Max
};
}
一般我们Type用Runtime,editor扩展的时候用Editor就好了。
而如果我们想使用插件中的某个模块,首先要启用这个插件,可在Plugins中添加插件。这里比如我把之前做的UCharts.uplugin删去“plugin”项,在生成的时候会报warning:
UnrealBuildTool : warning : Warning: Plugin 'UCharts' does not list plugin 'UGUI' as a dependency, but module 'UCharts' depends on 'UGUI'.
但是似乎仍然能正常使用。不过安全起见我还是加上了“plugin”项。
为什么UE要用C#来管理编译流程?
UE4支持众多平台,包括Windows,IOS,Android等,因此UE4为了方便你配置各个平台的参数和编译选项,简化编译流程,UE4实现了自己的一套编译系统,否则我们就得接受各个平台再单独配置一套项目之苦了。
这套工具的编译流程结果,简单来说,就是你在VS里的运行,背后会运行UE4的一些命令行工具来完成编译,其他最重要的两个组件:
- UnrealBuildTool(UBT,C#):UE4的自定义工具,来编译UE4的逐个模块并处理依赖等。我们编写的Target.cs,Build.cs都是为这个工具服务的。
- UnrealHeaderTool (UHT,C++):UE4的C++代码解析生成工具,我们在代码里写的那些宏UCLASS等和#include "*.generated.h"都为UHT提供了信息来生成相应的C++反射代码。
一般来说,UBT会先调用UHT会先负责解析一遍C++代码,生成相应其他代码。然后开始调用平台特定的编译工具(VisualStudio,LLVM)来编译各个模块。最后启动Editor或者是Game.
优点:C#足够易读,C#足够灵活定制逻辑,C#可以动态编译,方便搜集信息,C#足够强大可以调用其他工具
一次build的流程:
- UBT会首先收集目录里的cs文件,
- 第二步会调用UHT,UHT会分析.h和.cpp文件,UHT是一个文本解析工具而不是编译工具,会根据特定的标志如"*.generated.h"以及UFUNCTION宏等来生成相应的C++反射代码。生成的文件一般在Intermediate文件夹中。
- 最后UBT调用MSBuild去把项目中的.h和.cpp文件与生成的反射的.h和.cpp文件合在一起编译。
[YourModuleName]_API宏:
第一次做插件的时候出现了一个bug,半天编译不过去,后来通过一步步注释用排除法,终于发现编译错误的地方是源于我一开始的雷达图插件的类名前的[YourModuleName]_API没有大写。
![](https://pic3.zhimg.com/80/v2-085e2165be189386ed24af2053b17042_1440w.webp)
这个宏究竟有个啥用呢?我查看了源码,位于UBT的
UnrealBuildTool\Configuration\UEBuildModule.cs
![](https://pic3.zhimg.com/80/v2-bc3dbb02a7fc58e88a76afaa099eb486_1440w.webp)
![](https://pic1.zhimg.com/80/v2-d92f98f9f034c62aab90becee76ec0fc_1440w.webp)
由上可以看到,这里的Name就是UBT解析的时候的模块名,而readonly关键字使ModuleApiDefine只能赋值一次,之后便无法更改。
Name.ToUpperInvariant()将模块名转为大写,因此[YourModuleName]_API没有大写便会与ModuleApiDefine不对应,从而UBT无法完成正确的解析。
(事实上这个宏还可以在很多地方用于EndsWith检测,用来判断是否是DLL import/export API macro,如UHT的BaseParser:
![](https://pic3.zhimg.com/80/v2-66b6d5240134e132bcc5f707429e9d62_1440w.webp)
而这个宏最终的作用是做DLL导出的:
![](https://pic1.zhimg.com/80/v2-d334678660f427510e8a3658e7b56840_1440w.webp)
其放函数声明前用于暴露(导出)该函数,放类声明前用于暴露(导出)该类的所有内容。
再回到雷达图控件的实现,比如我把UCHARTS_API宏删去就会报错:
Module.UChartsEditor.cpp.obj : error LNK2019: 无法解析的外部符号 "private: static class UClass * __cdecl URadarChartComponent::GetPrivateStaticClass(void)"
这是因为我在UChartsEditor模块中调用了它,所以必须加_API宏
而若是不需要其它模块链接,则可以不加
ModuleRules.cs
在.Build.cs中,可以看到每个模块的类都继承着基类ModuleRules。
而在ModuleRules.cs中可以从class ModuleRules看到如PublicIncludePaths等的注释说明:
![](https://pic4.zhimg.com/80/v2-eb960261421c332f9d0fd713c9455f2f_1440w.webp)
模块链接:
PublicDependencyModuleNames:
- public链接的模块名称,最常用
- 在自己的public和private里包含对方的public
PrivateDependencyModuleNames:
- private链接的模块名称,只引用不暴露
- 在private里包含对方的public,不扩充自己的public
DynamicallyLoadedModuleNames:
- 动态链接的模块名称,在runtime被ModuleManager加载,保证先编译,用的较少
头文件include:
PublicIncludePaths:
- public包含的路径
- 定义自己向外暴露的public,默认”Public”和”Classes”
PrivateIncludePaths:
- private包含的路径
- 定义自己的private,给自己内部引用,默认“Private”,一般用来定义Private子目录。当然也可以路径包含Private/Sub,但这是一种方便方式。
头文件include模块:
PublicIncludePathModuleNames:
- public包含的模块名称,可以只写模块名称
PrivateIncludePathModuleNames:
- private包含的模块名称,可以只写模块名称
用途:
- 只包含对方模块的.h文件,比如模板,虽然挺少见
- 更多是动态链接,先包含头文件信息,之后加载
第三方库链接:
PublicAdditionalLibraries:
- 引用的第三方lib路径
PublicSystemLibraryPaths :
- 引用的系统lib路径,其实也只是lib,只不过对于一些更“系统”底层的库用这个名字更友好一些
PublicDelayLoadDLLs:
- 延迟加载的dll
还有的一些区别与联系:
可见一篇很好的文章:
UE4 模块,PrivateDependencyModuleNames?
public包含和private包含:
![](https://pic4.zhimg.com/80/v2-d288071ffe03dfaa6bfe5de146e9e903_1440w.webp)
思考:
为什么写模块都写Public文件夹和Private文件夹呢?除了清晰之外,还有一大原因应该是头文件include的默认就是这样。
在一个模块中include一个头文件的方法:
如果我们需要引用一个头文件,我们首先应当去找这个头文件属于哪个模块,然后可以在自己的相应的PrivateDependencyModuleNames中去添加这个模块(用private不会把它暴露给别人用),之后就可以直接include进来了。
Editor扩展:
在实习过程中,做了几个插件和一些编辑器扩展。
Details扩展:
不管是怎样实现,都一定会去重载接口:
![](https://pic1.zhimg.com/80/v2-6cf7f41b86b54c740e774d7183649634_1440w.webp)
而我们这里还添加了一个成员:
![](https://pic4.zhimg.com/80/v2-d4b1a94bd8b3106b47d7f1f55eb35c47_1440w.webp)
主要是用来接收DetailBuilder.GetObjectsBeingCustomized的返回值:
![](https://pic3.zhimg.com/80/v2-5166a40155498e105e1117c84ab4a8f6_1440w.webp)
这里我看源码还发现了一个有意思的现象:
![](https://pic3.zhimg.com/80/v2-b889d82db29358542a606312be072ade_1440w.webp)
可以看到这里的TArray< TWeakObjectPtr<UObject> >中间是有空格隔开,看着十分别扭,而源码中还有很多地方并未这样用空格隔开:
![](https://pic1.zhimg.com/80/v2-d165f3b5970686e9a3f3b23f9667ed30_1440w.webp)
究其原因我想是因为C++2.0以前是不能识别容器的两个">>"的,会和符号">>"混淆,而2.0之后就不会了。
UE4 DetailBuilder源码的简单剖析:
对 DetailBuilder 感到好奇,做了一点个人的理解分析,仅供参考,还望大佬指正:
这里
IDetailLayoutBuilder
类的Buider的含义是建造者模式,其想在IDetailLayoutBuilder类中提供各种“建造”的相关接口,如我们用到的GetObjectsBeingCustomized,然后会在FDetailLayoutBuilderImpl类中去实现这些接口。
而最终会把是实现类FDetailLayoutBuilderImpl以及一些其他数据用一个struct包起来(在PropertyRowGenerator.h中):
![](https://pic1.zhimg.com/80/v2-d7e70aca7a8ccba2b6a1a8416c6446d0_1440w.webp)
最后的指挥者则是DetailLayoutHelpers,在其中的UpdateSinglePropertyMapRecursive循环更新单个属性函数实现细节面板。
对于最终的产品如SDetailsViewBase在其cpp文件中则会使用DetailLayoutHelpers去“建造”:
![](https://pic4.zhimg.com/80/v2-3911893656a3e064b7808c6e054af667_1440w.webp)
而我们的实现CustomizeDetails中,
提供的参数是基类IDetailLayoutBuilder的函数指针,这是合理的,这里实际上,因为会进行动态绑定,参数为基类的指针会进行向上转型,保证安全。
所以这里实际上GetObjectsBeingCustomized是由FDetailLayoutBuilderImpl类去实现的。
关于自定义资源、导入重导入的一些实现:
TypeActions注册:
![](https://pic4.zhimg.com/80/v2-8e8130d33f6bb60dface60f498a752ff_1440w.webp)
主要是重载了一些方法。分别实现功能:
- 缩略图显示的颜色
- 打开资产的编辑器
- 返回资源名称,显示在缩略图中
- 告诉编辑器这个操作应该用于什么类,必须实现,否则编辑器不知道要定义什么资源 所属的Category
而若是想把Category改为自己定义的,则可以增加一个成员变量,然后于函数中返回。这里我直接用了引擎自带的类Basic,官方鼓励用引擎自带的类,因为一共最多出现32个类(官方已经占了12个)。
注册到资源工具:
这里我们还得在相关的EditorModule里注册,之后编辑器才知道有这样一种针对某资源的操作。
void YourEditorModule::StartupModule()
{
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
Action = MakeShareable(new FTextAssetTypeActions());
// 注册到资源工具里,之后编辑器才知道有这样一种针对某资源的操作
AssetTools.RegisterAssetTypeActions(Action.ToSharedRef());
}
UFactory注册:
- 实现了引擎面板里生成自定义资源。
其实就是重写一下两个函数:
![](https://pic4.zhimg.com/80/v2-e0d4f696c0a6b8c64bc365d468d26f4f_1440w.webp)
2. 实现引擎拖拽导入自定义资源以及重导入功能
先说简单的,重导入功能,其实就是实现一下函数:
![](https://pic1.zhimg.com/80/v2-612b66abea0729024bce4217070d0b3c_1440w.webp)
其中SetReimportPaths设置指定对象的重新导入路径,一般情况下我们不用写东西,只是因为是纯虚函数所以我们这里必须要进行重写。
CanReimport就直接返回true就可以了。
Reimport则比较套路,如下:
![](https://pic1.zhimg.com/80/v2-9feb1f24f3c6e095b25ca29012cf40cc_1440w.webp)
而拖拽导入其实也很套路,主要是重写:
![](https://pic4.zhimg.com/80/v2-defaf6e9e6f204e900786c199f87d96b_1440w.webp)
这里上面注释掉的是旧的API,我们一般写下面这个。
同时还要在构造函数声明
UTextAssetFactory::UTextAssetFactory(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
// 指定一个文件的后缀,编辑器见到这个后缀就会认为要使用这个工厂
// 一定要记得这一个分号!!!!
Formats.Add(FString(TEXT("json1;Font")));
SupportedClass = UTextAsset::StaticClass();
bCreateNew = false;
bEditorImport = true;
}
这里
Formats.Add(FString(TEXT("json1;Font")));
是告诉编译器看到后缀为json1就自动使用我们这个工厂类,这里json1后面的分号必须加,否则报错。
而真正在FactoryCreateFile实现的时候则需要对资源进行解析。
这里我主要是配合我的自定义类TextAsset,将json1中的信息给序列化进导入的TextAsset资源中。
可以看到在内部实现我先New了一个对象TextAsset
TextAsset = NewObject<UTextAsset>(InParent, InClass, InName, Flags);
然后根据json1格式去对应地解析,这里我使用了ue4自带的模块,引用了头文件Json.h和JsonObjectConverter.h
编辑器扩展小结:
UE4一共有六种编辑器扩展:
![](https://pic3.zhimg.com/80/v2-a2e7f67cb10f50cff067a103dd15ac26_1440w.webp)
从上图可以看到,六种分别为:
- 工具栏的扩展
- 菜单的扩展
- 细节面板的扩展
- 图表的扩展
- 自定义资源的扩展
- 编辑器模式的扩展
这里我只简单说了第三和第五,也就是细节面板的扩展和自定义资源的扩展。其余仍有待自己学习与补充。
参考资料:
[1] [中文直播]第12期 | 虚幻C++进阶之路 | Epic 大钊
[2] 【UE4】插件与模块
[3] UE4 模块,PrivateDependencyModuleNames?
[4] 插件创建和使用最佳实践
[5] 官方文档
[6] 【合集】UE4插件与Slate