Unreal 模块设计(Modules)

Unreal对C++代码有一套构建系统UBT(Unreal Build Tools),从引擎自身到用户的代码,都由UBT以Module为单位进行构建。一个Module相当于一个库,被UBT构建后,在运行时被加载和使用。UBT相当于UE的一套CMake+编译系统,一来隔离平台差异,二来用统一的方式来管理依赖比较优雅。下面罗列一些Unreal Module的特点(后面称Module为模块)。

结构和构建

模块的根目录有一个配置文件,名为YourModuleName.Build.cs,基本内容如下,声明了

// ...

// 声明预编译头的用法
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;    
// 声明公开依赖的其它模块,这里依赖了一些引擎的基本模块
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
// 声明私有依赖的其它模块
PrivateDependencyModuleNames.AddRange(new string[] {  });

// ...

构建时,UBT基于这些依赖关系去引用其它模块的头文件,并链接它们的代码,进行编译。Unreal也提供了工具(右键uproject文件)来生成对应IDE的工程文件(比如.sln或.xcodeproj),方便用IDE打开。

预编译头(PCHs)

预编译头是为了编译性能而设计的机制,将常用的、通用的、不经常变的头预先编译一次,后续只要不改变就不重新编译,从而提升编译性能。

用户定义模块时,可以选择创建自己的PCH,使用依赖模块的PCH,或不使用PCH。

上面的Build.cs文件中有定义PCHUsage,创建一个模块时的默认选项是UseExplicitOrSharedPCHs,绝大部分模块都使用UseExplicitOrSharedPCHsNoPCHs即可。

所谓使用SharedPCHs,就是由Engine在依赖的诸多模块中,选择一个最合适的模块所定义的预编译头。而Explicit的,就是使用本模块显式定义的预编译头

用户可以在一个模块的Build.cs中定义PrivatePCHHeaderFile = "xxx.h"SharedPCHHeaderFile = xxx.h

  • PrivatePCHHeaderFile是为当前模块指定的预编译头
  • SharedPCHHeaderFile则是为其它依赖本模块的其它模块提供的可选预编译头,不是给本模块用的

暴露性(给其它模块调用)

模块的根目录通常有Private和Public两个路径,想要暴露给其它模块使用的代码,要放在Public中,但还不够,需要对代码做进一步定义。

在UCLASS声明中添加MinimalAPI,可以允许该类在其它模块代码中被用于类型转换,或调用inline的函数

UCLASS(Blueprintable, MinimalAPI)

在成员变量/函数前声明本模块API字样,可以允许其它模块代码使用此成员变量/函数

HEHE_API void DoSomething();

在类名前声明本模块API字样,可以允许整个类被其它模块的代码使用

class HEHE_API AHeheActor : public AActor

可见性(public/private)

一个模块要使用另一个模块,需要:

  • 引用其头文件
  • 在Build.cs文件中的依赖中添加该模块(PublicDependencyModuleNamesPrivateDependencyModuleNames

除了C++本身的可见性,UBT在处理依赖时,也有一些可见性规则。

  • 单层依赖,一个模块A依赖另一个模块B,UBT在构建A模块时会去包含B模块的头文件,并链接B模块的代码
  • 多层依赖,模块A以依赖模块B,模块B以private依赖模块C,A并不会直接包含C的头文件或链接C的代码
  • 多层依赖,模块A以依赖模块B,模块B以public依赖模块C,A会直接包含C的头文件,但不会链接C的代码

所以在实际设计模块代码时,将只有本模块才使用的依赖项,作为PrivateDependencyModuleNames依赖,如果依赖项可能被本模块的用户使用,就应该加入到PublicDependencyModuleNames

实现

要让UE引擎识别模块,需要在源码的任意处声明IMPLEMENT_MODULEIMPLEMENT_GAME_MODULEIMPLEMENT_PRIMARY_GAME_MODULE,例如默认的C++游戏工程会自动生成一个模块名.cpp,里面定义了IMPLEMENT_PRIMARY_GAME_MODULE。一个游戏工程里通常只有一个一个IMPLEMENT_PRIMARY_GAME_MODULE

模块可以定义一个实现类,通过IMPLEMENT_MODULE方法注册给引擎,这个类中可以实现一些有用的回调,比如StartupModuleStartupModule如下

// Hehe.h
class FHeheModule : public IModuleInterface
{
    virtual void StartupModule() override;
    virtual void StartupModule() override;
}

// Hehe.cpp
IMPLEMENT_MODULE(FHeheModule, ModuleTest);

其它模块也可以调用实现类的成员函数

FModuleManager::Get().LoadModuleChecked(TEXT("Hehe")).DoSomething();

一个模块如果依赖其它GameModule,它就应该被声明为GameModule,GameModule的好处是热更新。如果希望做一个功能相对独立的,可以发布给其它人使用的模块,则可以定义为普通Module。定义为GameModule的模块如果重载了FDefaultModuleImpl实现类,还需要定义IsGameModule()方法来返回true

重要属性

模块要么在.uproject中加载,要么在.uplugin中加载。在这两个文件中,可以定义一些模块的属性:

  • Type:默认是Runtime,除了独立运行都可以,可以进一步限制模块的使用场景,仅编辑器或者仅服务器/客户端等,详见EHostType::Type
  • LoadingPhase:模块的加载阶段,默认是Default是在引擎初始化期间,GamePlay模块加载完后加载,详见ELoadingPhase::Type
  • IncludelistPlatforms / ExcludelistPlatforms: Win32, Win64, Mac, Linux, Android, IOS等
  • IncludelistTargets / ExcludelistTargets:Game, Server, Client, Editor, Program等
  • IncludelistTargetConfigurations / ExcludelistTargetConfigurations:ebug, DebugGame, Development, Shipping, Test等

详细属性列表可见Unreal Engine Modules。上述的Include/Exclude是UE5的属性,在UE4中叫做Whitelist/Blacklist。

参考

  • Unreal Engine Modules
  • ari.games
  • UE4 Modules by Ari Arnbjörnsson

你可能感兴趣的:(Unreal 模块设计(Modules))