UE4插件与一些编辑器扩展总结

发布时间 2023-05-23 11:36:36作者: tomato-haha

前言:

.uplugin与.uproject


前面的版本号、版本名、插件名等在编辑器下创建插件就会有对应生成。
值得一提的是"module"与"Plugins":


比如我做的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的流程:

  1. UBT会首先收集目录里的cs文件,
  2. 第二步会调用UHT,UHT会分析.h和.cpp文件,UHT是一个文本解析工具而不是编译工具,会根据特定的标志如"*.generated.h"以及UFUNCTION宏等来生成相应的C++反射代码。生成的文件一般在Intermediate文件夹中。
  3. 最后UBT调用MSBuild去把项目中的.h和.cpp文件与生成的反射的.h和.cpp文件合在一起编译。

 

[YourModuleName]_API宏:

第一次做插件的时候出现了一个bug,半天编译不过去,后来通过一步步注释用排除法,终于发现编译错误的地方是源于我一开始的雷达图插件的类名前的[YourModuleName]_API没有大写。

这个宏究竟有个啥用呢?我查看了源码,位于UBT的
UnrealBuildTool\Configuration\UEBuildModule.cs

由上可以看到,这里的Name就是UBT解析的时候的模块名,而readonly关键字使ModuleApiDefine只能赋值一次,之后便无法更改。

Name.ToUpperInvariant()将模块名转为大写,因此[YourModuleName]_API没有大写便会与ModuleApiDefine不对应,从而UBT无法完成正确的解析。

(事实上这个宏还可以在很多地方用于EndsWith检测,用来判断是否是DLL import/export API macro,如UHT的BaseParser:

而这个宏最终的作用是做DLL导出的

其放函数声明前用于暴露(导出)该函数,放类声明前用于暴露(导出)该类的所有内容。

再回到雷达图控件的实现,比如我把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等的注释说明:

模块链接:

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包含:

思考:

为什么写模块都写Public文件夹和Private文件夹呢?除了清晰之外,还有一大原因应该是头文件include的默认就是这样。

在一个模块中include一个头文件的方法:

如果我们需要引用一个头文件,我们首先应当去找这个头文件属于哪个模块,然后可以在自己的相应的PrivateDependencyModuleNames中去添加这个模块(用private不会把它暴露给别人用),之后就可以直接include进来了。

 

Editor扩展:

在实习过程中,做了几个插件和一些编辑器扩展。

Details扩展:

不管是怎样实现,都一定会去重载接口:

而我们这里还添加了一个成员:


主要是用来接收DetailBuilder.GetObjectsBeingCustomized的返回值:


这里我看源码还发现了一个有意思的现象:


可以看到这里的TArray< TWeakObjectPtr<UObject> >中间是有空格隔开,看着十分别扭,而源码中还有很多地方并未这样用空格隔开:


究其原因我想是因为C++2.0以前是不能识别容器的两个">>"的,会和符号">>"混淆,而2.0之后就不会了。

 

UE4 DetailBuilder源码的简单剖析:

对 DetailBuilder 感到好奇,做了一点个人的理解分析,仅供参考,还望大佬指正:

这里 IDetailLayoutBuilder 类的Buider的含义是建造者模式,其想在IDetailLayoutBuilder类中提供各种“建造”的相关接口,如我们用到的GetObjectsBeingCustomized,然后会在FDetailLayoutBuilderImpl类中去实现这些接口。
而最终会把是实现类FDetailLayoutBuilderImpl以及一些其他数据用一个struct包起来(在PropertyRowGenerator.h中):


最后的指挥者则是DetailLayoutHelpers,在其中的UpdateSinglePropertyMapRecursive循环更新单个属性函数实现细节面板。


对于最终的产品如SDetailsViewBase在其cpp文件中则会使用DetailLayoutHelpers去“建造”:


而我们的实现CustomizeDetails中,
提供的参数是基类IDetailLayoutBuilder的函数指针,这是合理的,这里实际上,因为会进行动态绑定,参数为基类的指针会进行向上转型,保证安全。
所以这里实际上GetObjectsBeingCustomized是由FDetailLayoutBuilderImpl类去实现的。

 

关于自定义资源、导入重导入的一些实现:

TypeActions注册:


主要是重载了一些方法。分别实现功能:

  • 缩略图显示的颜色
  • 打开资产的编辑器
  • 返回资源名称,显示在缩略图中
  • 告诉编辑器这个操作应该用于什么类,必须实现,否则编辑器不知道要定义什么资源 所属的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注册:

 

  1. 实现了引擎面板里生成自定义资源。

其实就是重写一下两个函数:

2. 实现引擎拖拽导入自定义资源以及重导入功能

先说简单的,重导入功能,其实就是实现一下函数:


其中SetReimportPaths设置指定对象的重新导入路径,一般情况下我们不用写东西,只是因为是纯虚函数所以我们这里必须要进行重写。
CanReimport就直接返回true就可以了。
Reimport则比较套路,如下:



而拖拽导入其实也很套路,主要是重写:


这里上面注释掉的是旧的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一共有六种编辑器扩展:

从上图可以看到,六种分别为:

  1. 工具栏的扩展
  2. 菜单的扩展
  3. 细节面板的扩展
  4. 图表的扩展
  5. 自定义资源的扩展
  6. 编辑器模式的扩展

这里我只简单说了第三和第五,也就是细节面板的扩展和自定义资源的扩展。其余仍有待自己学习与补充。

 

参考资料:

[1] [中文直播]第12期 | 虚幻C++进阶之路 | Epic 大钊

[2] 【UE4】插件与模块

[3] UE4 模块,PrivateDependencyModuleNames?

[4] 插件创建和使用最佳实践

[5] 官方文档

[6] 【合集】UE4插件与Slate