Module 是构成 Unreal 的基本元素,每一个 Module 封装和实现了一组功能,并且可以供其他的 Module 使用,整个 Unreal Engine 就是靠各个 Module 组合驱动的,连我们创建的游戏项目本身,都是一个单独的 Module。
那么 UE 又是怎么创建和构建这这些 Module 的呢?这是写这篇文章的主要目的,研究一下 Unreal 的构建系统以及它们 (Target 和 Module) 支持的各种属性。
建议在看这篇文章之前先看一下我之前的这篇文章:Build flow of the Unreal Engine4 project,主要内容是大致过一遍 UE 的构建流程,本篇文章只是 UE 构建系统中的一环。
对于 UE 项目比较熟悉的都知道,当使用 UE 创建一个 C++ 游戏项目时,会在项目路径下创建 Source
文件夹,默认包含了下列文件:
1 | Example\GWorld\Source>tree /a /f |
其中,*.Target.cs
与 *.Build.cs
是 Unreal 构建系统的实际控制者,UBT 通过扫描这两个文件来确定整个编译环境,它们也是本篇文章研究的重点。
它们的职责各不相同:
*.Target.cs
控制的是生成的可执行程序的外部编译环境,就是所谓的Target。比如,生成的是什么Type
(Game/Client/Server/Editor/Program),开不开启 RTTI(bForceEnableRTTI
),CRT 使用什么方式链接(bUseStaticCRT
) 等等。*.Build.cs
控制的是 Module 编译过程,由它来控制所属 Module 的对其他 Module 的依赖、文件包含、链接、宏定义等等相关的操作,*.Build.cs
告诉 UE 的构建系统,它是一个 Module,并且编译的时候要做哪些事情。
以一言以蔽之:与外部编译环境相关的都归 *.target.cs
管,与 Module 自身相关的都归 *.build.cs
管。
插个题外话,在 GWorld.h
和GWorld.cpp
中定义的是 Module 真正的执行逻辑,使用 IMPLEMENT_MODULE
定义。UE 中所有的 Module 都是继承自IModuleInterface
,具有以下接口:
1 | class IModuleInterface |
通过 IModuleInterface
来驱动 Module 的启动与关闭,不过一般 Game Module
不使用这个控制游戏流程。
这部分的详细内容可以看我之前的文章:UE4 Modules:Load and Startup
Target
每一个基于 Unreal 的项目,都有一个 Tergat.cs
,具有一个继承自TargetRules
的类定义;并且默认需要关联着一个同名 (非必要,但建议) 的Module
的定义,否则编译时会有 Module 未定义错误,它的含意时将指定的 Module
编译到 Target
中:
1 | UnrealBuildTool : error : Could not find definition for module 'GWorld' (referenced via GWorld.Target.cs) |
与 Target
关联的 Module
的名字可以通过 ExtraModuleNames
来指定:
1 | public class GWorldTarget : TargetRules |
上面指定的是 GWorld
,UBT 解析的时候就会去找GWorld
这个 Module 的定义,也就是 GWorld.build.cs
这个文件中的 GWorld
类定义,如果没有就会产生上面的 Module 未定义错误。
注意,与
Target
关联的Module
不仅仅只是一个指定的名字这么简单,所有代码中使用的XXXX_API
都是与Module
的名字相关的。
如果我进行以下改动:ExtraModuleNames.AddRange(new string[] { "GWorldAAA" } );
,那么需要对项目中所有的源文件进行的改动有:
- 将原有的
GWorld.build.cs
文件改名为GWorldAAA.build.cs
,并将文件内容的所有GWorld
替换为GWorldAAA
; - 将项目内所有头文件的
GWORLD_API
改名为GWORLDAAA_API
,因为XXX_API
的导出符号是依赖于ModuleName
的;
实在是个不小的工作量,所以还是建议将 ExtraModuleNames
中指定的名字与 Game Module
同名。
通过上面的内容,我们可以知道了 Target.cs
是如何与 Build.cs
关联的。那么,其实 Game
/Server
/Client
/Editor
的Target
可以共用同一个 Module
,将他们的ExtraModuleNames
都设置成同一个就可以了(如果你想要针对每个 Target 类型单独写也可以)。
TargetRules
的代码在 UnrealBuildTools/Configuration/ModuleRules(ReadOnlyTargetRules
也定义其中),可以看一下所支持参数的默认值;UE 对 Target
支持属性的描述文档:Targets。
但是 UE 的官方文档里面也只是代码里的注释,有些描述看了之后摸不着头脑,后面我会分析一下 TargetRule
一些属性的含义,先埋个坑。
Type(TargetType)
TargetRules
中的属性 Type
,其类型为TargetType
,定义为TargetRules.cs
中,是指定项目要编译出来的是什么程序。
- Game - A standalone game which requires cooked data to run.
- Client - Same as Game, but does not include any server code. Useful for networked games.
- Server - Same as Game, but does not include any client code. Useful for dedicated servers in networked games.
- Editor - A target which extends the Unreal Editor.
- Program - A standalone utility program built on top of the Unreal Engine.
LinkType(TargetLinkType)
TargetRules
中的 LinkType
,其类型为TargetLinkType
,定义在TargetRules.cs
中,是指定项目的链接类型。
TargetLinkType
具有三个枚举值:
1 | /// <summary> |
TargetLinkType.Default
是LinkType
的默认值,在此种状态下,如果当前Target
的Type
为Editor
则使用Modular
类型,链接所有的模块的方式为动态链接库。TargetLinkType.Modular
:以动态链接库的方式链接 ModuleTargetLinkType.Monolithic
:将所有的模块链接到单个文件(静态链接)
可以通过修改 LinkType
来修改。
1 | /// <summary> |
Name(string)
Target 的名字,只读属性,传进来的项目名字。
Platform(UnrealTargetPlatform)
Platform
的类型为UnrealTargetPlatform
,它是一个枚举,定义在UnrealBuildTool\Configuration\UEBuildTarget.cs
。
它记录着当前 Target 的平台信息,如 Win32/Win64 等等,目前 UE_4.22
的版本支持的平台为:
1 | public enum UnrealTargetPlatform |
我们可以在 build.cs
或者 target.cs
中通过判断 Platform 来做不同的事情。
如:
1 | if(Target.Platform != UnrealTargetPlatform.Win32 && Target.Platform != UnrealTargetPlatform.Win64) |
IsInPlatformGroup
这是一个函数 bool IsInPlatformGroup(UnrealPlatformGroup Group)
,定义在TargetRules.cs
中,它用来判断当前的 Platform
是否输入某一组。
需要传入的参数为 UnrealTargetformGroup
枚举类型,它定义在 UEBuildTarget.cs
中:
1 | /// <summary> |
Configuration(UnrealTargetConfiguration)
当前编译的配置,类型为 UnrealTargetConfiguration
的枚举,定义在 UEBuildTarget.cs
中,由 VS 中的 Configuration
构造而来,如:
- Development
- Shipping
- DebugGame
- Debug
- Test
- Unknow
也就是通过这个设置,UBT 才在编译环境中添加了下列宏:
1 | public override void SetUpConfigurationEnvironment(ReadOnlyTargetRules Target, CppCompileEnvironment GlobalCompileEnvironment, LinkEnvironment GlobalLinkEnvironment) |
Architecture(string)
所运行的平台的架构信息:x86
/arm
等等。
CppStandard(CppStandardVersion)
用于指定编译项目时所用的 C++ 标准版本(在新版本引擎 (4.23) 中才有)。CppStandardVersion
:
Latast
Cpp17
- Cpp14
这个选项本质上就是将 /std:c++xxx
添加到 VS 的编译选项中。
1 | void AppendCLArguments_CPP(CppCompileEnvironment CompileEnvironment, List<string> Arguments) |
bUseDebugCRT(bool)
用来控制输出的 Runtime Librart
类型是 MT
还是 MD
;
还用来控制添加 _DEBUG
和NODEBUG
宏:
1 | public override void SetUpConfigurationEnvironment(ReadOnlyTargetRules Target, CppCompileEnvironment GlobalCompileEnvironment, LinkEnvironment GlobalLinkEnvironment) |
ProjectDefinitions(List<string>)
为当前项目添加的宏定义,在整个项目中可用。
GlobalDefinitions(List<string>)
添加在整个 Target 中都可以用的宏定义。
bShouldCompileAsDLL(bool)
将 Target 编译为 DLL,为 true
时要求 LinkType
为Monolithic
。
1 | /// <summary> |
AdditionalCompilerArguments(String)
传递给编译器的参数。
AdditionalLinkerArguments(String)
传递给连接器的参数。
bUsesSlate(bool)
控制打包时时候把 Slate
相关的图片资源打包到 pak 中。
Module
与 Target
类似,每一个 Unreal 的 Module
,都有一个专属的ModuleName.Build.cs
里面定义着专属的 ModuleName
类,它由 ModuleRules
继承而来,我们对 Module
构建时进行的操作就是通过它来控制。
注意:不管是 Game Module 还是 Plugin Module,只要是 项目依赖 的 Module,编译时它们都会接收到当前使用的
Target
信息。
ModuleRules
的代码在 UnrealBuildTools/Configuration/ModuleRules,同样可以看一下支持的属性默认值;UE 对Modules
描述的官方文档:Modules,这里也同样只有代码的注释内容,没有实际例子,我就先来分析一些在工程中常见的 Build.cs 中属性的含义。
*.Build.cs
中可以通过它构造接收的 ReadOnlyTargetRules Target
参数来获取 *.Target.cs
中的属性信息。
1 | using UnrealBuildTool; |
通过 Target
对象,可以在 *.build.cs
中控制对不同的平台(Platform),架构(Architecture),以及其他的选项来对 Module 进行不同的操作(比如定义不同的宏 / 包含不同的 ThridParty/ 链接不同的 Lib 等等)。
ModuleDirectory
string ModuleDirectory
:项目的源码路径PROJECT_NAME/Source/PROJECT_NAME
的绝对路径。
EngineDirectory
string EngineDirectory
:引擎目录Engine/
在当前环境的绝对路径。
PublicAdditionalLibraries
添加静态链接库文件 (注意与PublicLibraryPaths
的区别),一般是用于第三方库的链接。
1 | PublicAdditionalLibraries.AddRange( |
详细的内容可以看:Linking Static Libraries Using The Build System
同样可以用在 DLL 的导入库,与
PublicDelayLoadDLLs
和RuntimeDependencies
配合使用。
PublicAdditionalShadowFiles
当执行远程编译的时候,指定当前模块需要复制到远程服务器上的文件,确保能够链接成功。
如远程打包 IOS 平台时,需要把当前模块依赖的静态链接库添加到里面(如 Game 模块依赖某个插件中的 External
模块)。
RuntimeDependencies
list<RuntimeDependency> RuntimeDependencies
:Module 在运行时依赖的文件 (.so
/.dll
等),打包时将会拷贝到存储目录。
在打包 Windows 时会直接把文件拷贝到打包的对应目录下,但是在 Android 上会把文件放到 Apk 包的 main.obb.webp 中。
PublicDelayLoadDLLs
List<string> PublicDelayLoadDLLs
:延迟加载的 DLL 列表,通常用于第三方库。
1 | // build.cs |
含义是不在程序启动时立即加载 DLL 的列表,等到首次需要使用他们的符号后再进行加载。这样可以在模块的 StartupModule
中自行指定位置并加载他们,从而实现可以不把 dll 放到 exe 的目录。
1 | FString AbsPath = FileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*MyLibPath); |
PS:配合
PublicAdditionalLibraries
可以用在使用 DLL 导入库的第三方库。
- 使用 PublicAdditionalLibraries 添加 lib
- DLL 的名字添加至 PublicDelayLoadDLLs
- 使用 RuntimeDependencies 打包时拷贝 dll
- 如果拷贝到的目录不是 exe 路径,需要 StartupModule 里执行
AddDllDirectory
和PushDllDirectory
把 dll 的路径添加到里面PublicDelayLoadDLLs 只添加 xxxx.dll 就可以了,不需要路径。
我使用 GoogleInstantPreview
测试使用 DLL+ 导入库并将 DLL 放在非 exe 目录的例子:ue4-plugin-GoogleInstanceIns.7z
PublicDefinitions
List<string> PublicDefinitions
:为当前 Module 添加公开宏定义,等同于传统 VS 项目在项目设置中添加一个预处理宏。
它被 UBT 分析之后会在产生一个 Definitions.PROJECT_NAME.h
的头文件,里面定了各种宏。
1 | Intermediate\Build\Win64\UE4Editor\Development\ReflectionExample\Definitions.ReflectionExample.h |
PublicSystemIncludePaths
List<string> PublicSystemIncludePaths
:文档介绍是用于添加系统的 Include 路径,与PublicIncludePaths
的区别是会跳过头文件解析检查(但是经我测试,使用这种方式包含的代码依然会检测下列错误(UE_4.20)):
1 | error : Expected mpack-platform.h to be first header included. |
注意:如果不指定路径,则默认的 IncludePath 路径是
Engine/Source
。
比如:
1 | PublicSystemIncludePaths.AddRange( |
它表示的路径是:
1 | D:\UnrealEngine\Epic\UE_4.21\Engine\Source\TEST_LIB |
所有可以在 *.build.cs
中指定的*IncludePaths
,默认的路径都是Engine/Source
.
PrivateRuntimeLibraryPaths
List<string> PrivateRuntimeLibraryPaths
:运行时库的搜索路径。例如.so
或者.dll
。
PublicRuntimeLibraryPaths
List<string> PublicRuntimeLibraryPaths
:运行时库的搜索路径。例如.so
或者.dll
。
因为动态链接库的查找路径默认只有:
- 系统的 PATH 路径;
- 可执行程序的当前目录;
如果我们的动态链接库在其他的位置,运行时就会错误,可以通过 PublicRuntimeLibraryPaths
或者 PrivateRuntimeLibraryPaths
来添加。
PublicLibraryPaths
添加链接库文件的 路径,如在源码中使用的:
1 |
可以通过 PublicLibraryPaths
来添加依赖的 Lib。
DynamicallyLoadedModuleNames
List<string> DynamicallyLoadedModuleNames
:添加需要运行时动态加载的 Module,使用FModuleManager::LoadModuleChecked<MODULE_TYPE>(TEXT("MODULE_NAME"))
等函数启动。
1 | // e.g |
PublicDependencyModuleNames
List<string> PublicDependencyModuleNames
:添加对执行 Module 的源文件依赖,自动添加所依赖 Module 的Public
和Private
源文件包含。
PrivateDependencyModuleNames
List<string> PrivateDependencyModuleNames
:与PublicDependencyModuleNames
不同的是,意味着所依赖的 Module 中的源文件只可以在Private
中使用。
假如现在有一个模块 A,还有一个模块 B,他们中都是 UE 的 Module/Public
和Module/Private
的文件结构。
- 如果 B 中依赖 A,如果使用的是
PrivateDependencyModuleNames
的方式添加的依赖,则 A 模块的源文件只可以在 B 的Private
目录下的源文件中使用,在Public
目录下的源文件使用时会报No such file or directory
的错误。 - 如果使用的是
PublicDependencyModuleNames
方式添加的依赖,则 A 的源文件在 B 的Public
与Private
中都可用。
除了上述的区别之外,还影响依赖于 B 模块的模块 ,当一个模块 C 依赖模块 B 的时候,只能访问到 B 模块的 PublicDependencyModule 中的模块暴露出来的类。
例如,C 依赖 B,B 依赖 A;那么,假如 C 想访问 A 中的类则有两种方式:
- 在 C 的依赖中添加上 A 模块
- 确保 B 在
PublicDependencyModuleNames
依赖中添加的 A 模块,这样 C 就可以间接的访问到 A。
经过测试发现,其实对于 游戏模块 (PROJECT_NAME/Source/PROJECT_NAME.target.cs
) 使用而言,所依赖的模块是使用 PublicDependencyModuleNames
还是 PrivateDependencyModuleNames
包含,没什么区别。
使用 Private
方式依赖的 Module 中的头文件依然可以在游戏模块的 Public
中用,这一点与插件等其他模块有所不同(但是这只有在所依赖的模块不是 bUsePreCompiled
的基础上的,如果所依赖的模块是 bUsePreCompiled
的,则与其他的模块一样,PrivateDependencyModuleNames
依赖的模块不可以在 Pulibc
目录下的源文件使用),这个行为比较奇怪:有时候出错有时又不出错。
注意:在游戏项目中使用依赖其他 Module 时尽量确定性需求地使用
PrivateDependencyModuleNames
或者PublicDependencyModuleNames
,在组合其他的选项时可能会有一些奇怪的行为。
相关的讨论:
- What is the difference between PublicDependencyModuleNames and PrivateDependencyModuleNames
- Explanation of Source Code folder structure?
bPreCompile 与 bUsePreCompiled
1 | /// <summary> |
这个两个属性需要组合来使用。
考虑下列需求:
如果我们写好的一个模块 A 希望拿给别人来用,但是又不想把所有代码开放出来,该怎么办?
在传统的 C++ 领域,我应该会说:把代码编译成 DLL,然后把头文件和 DLL 发放给用户就可以啦。
对!其实 bPreCompile
和bUsePreCompiled
就是做的类似的事情。
当我们对模块 A 进行编译之前,在它的 *.build.cs
中添加:
1 | public class A : ModuleRules |
然后编译模块 A。编译完成之后,将模块 A 的 Source/Private
删除 (删除之前请确保你已经备份),然后删除模块目录下的Intermediate
,但是要保留Binaries
目录。
最后,打开模块 A 的 A.build.cs
,将bPreCompile=true;
删掉,然后再添加:
1 | public class A : ModuleRules |
此时我们想要实现的目标都已经完成了:不发布实现代码(Private
),发布预先编译好的二进制,但是这样无法进行静态链接,如果只是暴露给蓝图使用可以,在其他的 Module 中使用它的符号会有符号未定义错误。
OptimizeCode(CodeOptimization)
这个属性是用来控制当前模块是否要开启优化代码,在我们用 VS 调试时,有时候会看到“变量已被优化,因而不可用”,这就是因为被优化了。
可以使用它来关闭优化:
1 | // build.cs |
CodeOptimization
支持几种值,默认是Default
,开启优化:
- Never
- Default
- InNonDebugBuilds
- InShippingBuildsOnly
相关的代码:
1 | // UnrealBuildTool/Configutation/UEBuildModuleCPP.cs |
这个函数在 UEBuildModuleCPP.cs
的CreateModuleCompileEnvironment
中调用,将结果赋值给了 CppCompileEnvironment.bOptimizeCode
,进而又在VCToolChain.cs
中被使用:
1 | UnrealBuildTool\Platform\Windows\VCToolChain.cs |
可以看到,在 Debug
的环境下,是默认关闭优化的。在非 Debug
时根据 CompileEnvironment.bOptimizeCode
的值来决定是否开启优化。
调试效果:
当使用默认时(OptimizeCode = CodeOptimization.Default;
):
当关闭代码优化时(OptimizeCode = CodeOptimization.Never;
):
建议使用OptimizeCode = CodeOptimization.InShippingBuildsOnly;
。
注意:这个选项和普通的 C++ 项目在 VS 中的
Properties
-Configuration
-C/C++
-Optimization
-Optimization
的设置时一样的。
bEnableUndefinedIdentifierWarnings (bool)
是否启用在预处理代码 #if
中使用未定义标识符的警告。
1 |
如果这个宏未定义,在启用 bEnableUndefinedIdentifierWarnings
的情况下会产生 C4688
错误。
相关的代码时定义在 UBT 的代码中的:
1 | // Source\Programs\UnrealBuildTool\Platform\Windows\VCToolChain.cs |
bUseRTTI (bool)
UE4 默认关闭了 RTTI,所以在工程的代码中写了类似 typeid
的代码,会产生下列错误:
1 | In file included from C:\UnrealProject_\Source\GWorld\Private\Modules\Flibs\FLibIniConfigHelper.cpp:10: |
解决办法只有两个:去掉 rtti
相关的代码,或者在当前 Module
的build.cs
中把 bUseRTTI
设置为true
。