1. 重要概念
1.1. Target
Target 即编译目标,UE 支持多种编译 target 类型
public enum TargetType
{
/// <summary>
/// Cooked monolithic game executable (GameName.exe). Also used for a game-agnostic engine executable (UE4Game.exe or RocketGame.exe)
/// </summary>
Game,
/// <summary>
/// Uncooked modular editor executable and DLLs (UE4Editor.exe, UE4Editor*.dll, GameName*.dll)
/// </summary>
Editor,
/// <summary>
/// Cooked monolithic game client executable (GameNameClient.exe, but no server code)
/// </summary>
Client,
/// <summary>
/// Cooked monolithic game server executable (GameNameServer.exe, but no client code)
/// </summary>
Server,
/// <summary>
/// Program (standalone program, e.g. ShaderCompileWorker.exe, can be modular or monolithic depending on the program)
/// </summary>
Program,
}
每一个 xx.target.cs 都可以配置某种 targetType 编译时的依赖关系与构建规则。
1.2. Module
模块是UE中用于组织代码的一种方式,引擎就是以大量模块的集合形式实现的。每个模块可以包含源文件、资源和第三方库等,它们可以被编译成静态或动态链接库(.lib或.dll文件)。模块定义了一组功能,可以被其他模块所依赖。通过**.build.cs
可以定义一个模块,在**.build.cs
中声明类,同时设置属性用以控制如何构建。整个工程实际上也可看做一个模块,可以在Source
目录下找到控制整个工程构建的.build.cs
。各个build.cs
通过UBT管理,从而确定了整个工程的编译环境。
1.3 target.cs 与 build.cs 区别
build.cs 控制模块内的依赖关系,target.cs控制编译可执行文件时包含依赖的模块,层级上应该是 build.cs < target.cs
1.4. UBT
UBT,即 UnrealBuildTool,编译 UE 工程时实际上就是利用命令行调用了UBT.exe,上面提到的target.cs以及build.cs的扫描与处理都是由 UBT 完成的。可以在 VS 的编译配置中找到点击build时触发的build.bat,其中就对应了带命令行参数启动UBT。
\Build.bat -Target="SherylTestEditor Win64 Development -Project=\"$(SolutionDir)SherylTest.uproject\"" -Target="ShaderCompileWorker Win64 Development -Quiet" -WaitMutex -FromMsBuild
// 通过命令行解析编译参数
// Parse the command line arguments
CommandLineArguments Arguments = new CommandLineArguments(ArgumentsArray);
// Parse the global options
GlobalOptions Options = new GlobalOptions(Arguments);
// 根据命令行参数找到对应的 UBT Mode,执行对应 Mode 的 Excute方法
//
// 1. eg. 处理模块间依赖关系,生成项目文件
// 调用 GenerateProjectFilesMode 内的方法,搜索项目内所有的 target 和 module,生成项目代码
//
// 2.eg. 编译工程 // todo: 找到build 入口,如何调用到BuildMode.cs
// 3.
2. 模块编译
2.1. 含义
UE 引擎和游戏都是以模块来组织的,模块之间可以相互调用,通过模块化的组织形式可以很好地解耦合。
2.2. 实现方式
2.2.1. 模块的管理
虚幻构建系统通过*.build.cs
和*.target.cs
声明的内容来构建工程,所有的模块都需要由对应的build.cs
来声明,其中,模块的定义需要从ModuleRules
继承。
各个模块的链接方式由TargetLinkType
指定,对应的定义如下
/// <summary>
/// Specifies how to link all the modules in this target
/// </summary>
[Serializable]
public enum TargetLinkType
{
/// <summary>
/// Use the default link type based on the current target type
/// </summary>
Default,
/// <summary>
/// Link all modules into a single binary
/// </summary>
Monolithic,
/// <summary>
/// Link modules into individual dynamic libraries
/// </summary>
Modular,
}
在*.target.cs
中指定不同的TargetLinkType指定动态(Modular)或静态(Monolithic)地加载需要使用的模块。
通过FModuleManager
来统一管理工程里的模块。相关实现的地址为\Engine\Source\Runtime\Core\Private\Modules
,Module 的管理与IS_MONOLITHIC
宏有关,这个宏由 UBT 定义,用来指示是否有 DLL 参与编译,源码中的注释为Whether we want a monolithic build (no DLLs); must be defined by UBT
。
在模块A的外部想要使用模块A可以通过 ModuleManager 的 LoadModule 或者 UnLoadModule方法。LoadModule 方法返回一个 IModuleInterface 指针,也就是说如果自己实现一个模块,那么自定义的类需要继承 IModuleInterface。
/**
* Loads the specified module.
*
* @param InModuleName The base name of the module file. Should not include path, extension or platform/configuration info. This is just the "module name" part of the module file name. Names should be globally unique.
* @return The loaded module, or nullptr if the load operation failed.
* @see AbandonModule, IsModuleLoaded, LoadModuleChecked, LoadModulePtr, LoadModuleWithFailureReason, UnloadModule
*/
IModuleInterface* LoadModule( const FName InModuleName );
// LoadModule的对应实现
IModuleInterface* FModuleManager::LoadModule( const FName InModuleName )
{
// We allow an already loaded module to be returned in other threads to simplify
// parallel processing scenarios but they must have been loaded from the main thread beforehand.
if(!IsInGameThread())
{
return GetModule(InModuleName);
}
EModuleLoadResult FailureReason;
// LoadModule 具体实现与 Module 的链接方式有关
IModuleInterface* Result = LoadModuleWithFailureReason(InModuleName, FailureReason );
// This should return a valid pointer only if and only if the module is loaded
checkSlow((Result != nullptr) == IsModuleLoaded(InModuleName));
return Result;
}
在LoadModule时,区分 Module 不同链接类型,若为静态链接,则去一个全局的StaticallyLinkedModuleInitializers
中寻找对应Module;若为动态链接,则直接加载对应 DLL。
2.2.2. 模块的实现
在实现一个对其他模块公开的模块时需要继承 IModuleInterface,并且在所有的 include 头文件声明后,调用 IMPLEMENT_MODULE 宏,宏定义可在路径\Engine\Source\Runtime\Core\Public\Modules\ModuleManager.h
中找到。
/**
* Module implementation boilerplate for regular modules.
*
* This macro is used to expose a module's main class to the rest of the engine.
* You must use this macro in one of your modules C++ modules, in order for the 'InitializeModule'
* function to be declared in such a way that the engine can find it. Also, this macro will handle
* the case where a module is statically linked with the engine instead of dynamically loaded.
*
* This macro is intended for modules that do NOT contain gameplay code.
* If your module does contain game classes, use IMPLEMENT_GAME_MODULE instead.
*
* Usage: IMPLEMENT_MODULE(<My Module Class>, <Module name string>)
*
* @see IMPLEMENT_GAME_MODULE
*/
#if IS_MONOLITHIC
// If we're linking monolithically we assume all modules are linked in with the main binary.
#define IMPLEMENT_MODULE( ModuleImplClass, ModuleName ) \
/** Global registrant object for this module when linked statically */ \
static FStaticallyLinkedModuleRegistrant< ModuleImplClass > ModuleRegistrant##ModuleName( TEXT(#ModuleName) ); \
/* Forced reference to this function is added by the linker to check that each module uses IMPLEMENT_MODULE */ \
extern "C" void IMPLEMENT_MODULE_##ModuleName() { } \
PER_MODULE_BOILERPLATE_ANYLINK(ModuleImplClass, ModuleName)
#else
#define IMPLEMENT_MODULE( ModuleImplClass, ModuleName ) \
\
/**/ \
/* InitializeModule function, called by module manager after this module's DLL has been loaded */ \
/**/ \
/* @return Returns an instance of this module */ \
/**/ \
extern "C" DLLEXPORT IModuleInterface* InitializeModule() \
{ \
return new ModuleImplClass(); \
} \
/* Forced reference to this function is added by the linker to check that each module uses IMPLEMENT_MODULE */ \
extern "C" void IMPLEMENT_MODULE_##ModuleName() { } \
PER_MODULE_BOILERPLATE \
PER_MODULE_BOILERPLATE_ANYLINK(ModuleImplClass, ModuleName)
#endif //IS_MONOLITHIC
IMPLEMENT_MODULE 这个宏的作用是将模块的主要类暴露给UE4引擎,使得模块可以被引擎识别和加载。
3. 模块的依赖传递
如果项目没有依赖模块 A ,那么在编译过程中模块 A即使在工程中也不会参与编译。
1、 Dependency 与 IncludePath
通过向以及PrivateDependencyModuleNames
和PublicDependencyModuleNames
添加另一个模块B的名称,可以指定一个模块对另外一个模块的依赖,该定义在\Engine\Source\Programs\UnrealBuildTool\Configuration\ModuleRules.cs
;与之相似的还有PublicIncludePathModuleNames
和PrivateIncludePathModuleNames
,这两个列表用于声明某个模块需要依赖的其他模块的头文件,编译时如果符号只定义在其他模块的cpp文件中则会有Undefined symbols。这两组 list 名称区别就在于时对模块(包含了cpp实现)整体的依赖还是对模块头文件(不包含cpp实现)的依赖。
2、 Public 与 Private
假设有三个模块A\B\C,使用public 或 private list区分第一点在于是本模块A的 public 代码还是 private 代码需要引入对模块 B 的依赖;第二点则是模块 C 依赖本模块 A 时,若使用的是 private 传递的依赖,则无法依赖到模块 B,反之,若用的是 public ,则可以依赖到模块 B。
4. 控制模块内符号的导出
使用{模块名}_API
来修饰一个类,可以控制这个类在编译时导出符号。
以下是使用_API宏的一些关键点:
-
模块化编译:当引擎以模块化方式编译时(即面向桌面平台的DLL文件, IS_MONOLITHIC为False),_API 宏才有效。
-
单片模式:与模块化模式相对的是单片模式(Monolithic Mode),它将所有代码放入同一个可执行文件中。这种模式由UnrealBuildTool(UBT)和/或平台及编译配置控制。
-
宏的实际效果:根据UBT的编译方式,_API 宏相当于以下某种形式:
__declspec(dllexport):当以模块模式编译模块代码时。
__declspec(dllimport):当引入一个模块的公开模块头信息时。 -
空:当以单片模式编译时。