UE 构建系统:Target 和 Module

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
2
3
4
5
6
7
8
9
10
Example\GWorld\Source>tree /a /f
| GWorld.Target.cs
| GWorldEditor.Target.cs
|
\---GWorld
GWorld.Build.cs
GWorld.cpp
GWorld.h
GWorldGameModeBase.cpp
GWorldGameModeBase.h

其中,*.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.hGWorld.cpp中定义的是 Module 真正的执行逻辑,使用 IMPLEMENT_MODULE 定义。UE 中所有的 Module 都是继承自IModuleInterface,具有以下接口:

1
2
3
4
5
6
7
8
9
10
11
12
class IModuleInterface
{
public:
virtual ~IModuleInterface();
virtual void StartupModule();
virtual void PreUnloadCallback();
virtual void PostLoadCallback();
virtual void ShutdownModule();
virtual bool SupportsDynamicReloading();
virtual bool SupportsAutomaticShutdown();
virtual bool IsGameModule(); const
};

通过 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
2
3
4
5
6
7
8
public class GWorldTarget : TargetRules
{
public GWorldTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
ExtraModuleNames.AddRange(new string[] { "GWorld" } );
}
}

上面指定的是 GWorld,UBT 解析的时候就会去找GWorld 这个 Module 的定义,也就是 GWorld.build.cs 这个文件中的 GWorld 类定义,如果没有就会产生上面的 Module 未定义错误。

注意,与 Target 关联的 Module 不仅仅只是一个指定的名字这么简单,所有代码中使用的 XXXX_API 都是与 Module 的名字相关的。

如果我进行以下改动:ExtraModuleNames.AddRange(new string[] { "GWorldAAA" } );,那么需要对项目中所有的源文件进行的改动有:

  1. 将原有的 GWorld.build.cs 文件改名为 GWorldAAA.build.cs,并将文件内容的所有GWorld 替换为GWorldAAA
  2. 将项目内所有头文件的 GWORLD_API 改名为 GWORLDAAA_API,因为XXX_API 的导出符号是依赖于 ModuleName 的;

实在是个不小的工作量,所以还是建议将 ExtraModuleNames 中指定的名字与 Game Module 同名。
通过上面的内容,我们可以知道了 Target.cs 是如何与 Build.cs 关联的。那么,其实 Game/Server/Client/EditorTarget可以共用同一个 Module,将他们的ExtraModuleNames 都设置成同一个就可以了(如果你想要针对每个 Target 类型单独写也可以)。

TargetRules的代码在 UnrealBuildTools/Configuration/ModuleRulesReadOnlyTargetRules 也定义其中),可以看一下所支持参数的默认值;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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// <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,
}
  • TargetLinkType.DefaultLinkType 的默认值,在此种状态下,如果当前 TargetTypeEditor 则使用 Modular 类型,链接所有的模块的方式为动态链接库。
  • TargetLinkType.Modular:以动态链接库的方式链接 Module
  • TargetLinkType.Monolithic:将所有的模块链接到单个文件(静态链接)

可以通过修改 LinkType 来修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// <summary>
/// Backing storage for the LinkType property.
/// </summary>
[RequiresUniqueBuildEnvironment]
[CommandLine("-Monolithic", Value ="Monolithic")]
[CommandLine("-Modular", Value ="Modular")]
TargetLinkType LinkTypePrivate = TargetLinkType.Default;

/// <summary>
/// Specifies how to link modules in this target (monolithic or modular). This is currently protected for backwards compatibility. Call the GetLinkType() accessor
/// until support for the deprecated ShouldCompileMonolithic() override has been removed.
/// </summary>
public TargetLinkType LinkType
{
get
{
return (LinkTypePrivate != TargetLinkType.Default) ? LinkTypePrivate : ((Type == global::UnrealBuildTool.TargetType.Editor) ? TargetLinkType.Modular : TargetLinkType.Monolithic);
}
set
{
LinkTypePrivate = value;
}
}

Name(string)

Target 的名字,只读属性,传进来的项目名字。

Platform(UnrealTargetPlatform)

Platform的类型为UnrealTargetPlatform,它是一个枚举,定义在UnrealBuildTool\Configuration\UEBuildTarget.cs

它记录着当前 Target 的平台信息,如 Win32/Win64 等等,目前 UE_4.22 的版本支持的平台为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public enum UnrealTargetPlatform
{
/// <summary>
/// Unknown target platform
/// </summary>
Unknown,

/// <summary>
/// 32-bit Windows
/// </summary>
Win32,

/// <summary>
/// 64-bit Windows
/// </summary>
Win64,

/// <summary>
/// Mac
/// </summary>
Mac,

/// <summary>
/// XboxOne
/// </summary>
XboxOne,

/// <summary>
/// Playstation 4
/// </summary>
PS4,

/// <summary>
/// iOS
/// </summary>
IOS,

/// <summary>
/// Android
/// </summary>
Android,

/// <summary>
/// HTML5
/// </summary>
HTML5,

/// <summary>
/// Linux
/// </summary>
Linux,

/// <summary>
/// All desktop platforms
/// </summary>
AllDesktop,

/// <summary>
/// TVOS
/// </summary>
TVOS,

/// <summary>
/// Nintendo Switch
/// </summary>
Switch,

/// <summary>
/// NDA'd platform Quail
/// </summary>
Quail,

/// <summary>
/// Confidential platform
/// </summary>
Lumin,
}

我们可以在 build.cs 或者 target.cs 中通过判断 Platform 来做不同的事情。

如:

1
2
3
4
if(Target.Platform != UnrealTargetPlatform.Win32 && Target.Platform != UnrealTargetPlatform.Win64)
{
PublicDefinitions.Add("HAVE_PTHREAD");
}

IsInPlatformGroup

这是一个函数 bool IsInPlatformGroup(UnrealPlatformGroup Group),定义在TargetRules.cs 中,它用来判断当前的 Platform 是否输入某一组。

需要传入的参数为 UnrealTargetformGroup 枚举类型,它定义在 UEBuildTarget.cs 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/// <summary>
/// Platform groups
/// </summary>
public enum UnrealPlatformGroup
{
/// <summary>
/// this group is just to lump Win32 and Win64 into Windows directories, removing the special Windows logic in MakeListOfUnsupportedPlatforms
/// </summary>
Windows,

/// <summary>
/// Microsoft platforms
/// </summary>
Microsoft,

/// <summary>
/// Apple platforms
/// </summary>
Apple,

/// <summary>
/// making IOS a group allows TVOS to compile IOS code
/// </summary>
IOS,

/// <summary>
/// Unix platforms
/// </summary>
Unix,

/// <summary>
/// Android platforms
/// </summary>
Android,

/// <summary>
/// Sony platforms
/// </summary>
Sony,

/// <summary>
/// Target all desktop platforms (Win64, Mac, Linux) simultaneously
/// </summary>
AllDesktop,
}

Configuration(UnrealTargetConfiguration)

当前编译的配置,类型为 UnrealTargetConfiguration 的枚举,定义在 UEBuildTarget.cs 中,由 VS 中的 Configuration 构造而来,如:

  • Development
  • Shipping
  • DebugGame
  • Debug
  • Test
  • Unknow

也就是通过这个设置,UBT 才在编译环境中添加了下列宏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public override void SetUpConfigurationEnvironment(ReadOnlyTargetRules Target, CppCompileEnvironment GlobalCompileEnvironment, LinkEnvironment GlobalLinkEnvironment)
{
// other code
UnrealTargetConfiguration CheckConfig = Target.Configuration;
switch (CheckConfig)
{
default:
case UnrealTargetConfiguration.Debug:
GlobalCompileEnvironment.Definitions.Add("UE_BUILD_DEBUG=1");
break;
case UnrealTargetConfiguration.DebugGame:
// Default to Development; can be overridden by individual modules.
case UnrealTargetConfiguration.Development:
GlobalCompileEnvironment.Definitions.Add("UE_BUILD_DEVELOPMENT=1");
break;
case UnrealTargetConfiguration.Shipping:
GlobalCompileEnvironment.Definitions.Add("UE_BUILD_SHIPPING=1");
break;
case UnrealTargetConfiguration.Test:
GlobalCompileEnvironment.Definitions.Add("UE_BUILD_TEST=1");
break;bUseDebugCRT
}
// other code
}

Architecture(string)

所运行的平台的架构信息:x86/arm等等。

CppStandard(CppStandardVersion)

用于指定编译项目时所用的 C++ 标准版本(在新版本引擎 (4.23) 中才有)。
CppStandardVersion

  • Latast
  • Cpp17
  • Cpp14

这个选项本质上就是将 /std:c++xxx 添加到 VS 的编译选项中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void AppendCLArguments_CPP(CppCompileEnvironment CompileEnvironment, List<string> Arguments)
{
// other code...
if(CompileEnvironment.CppStandard >= CppStandardVersion.Latest)
{
Arguments.Add("/std:c++latest");
}
else if(CompileEnvironment.CppStandard >= CppStandardVersion.Cpp17)
{
Arguments.Add("/std:c++17");
}
else if(CompileEnvironment.CppStandard >= CppStandardVersion.Cpp14)
{
Arguments.Add("/std:c++14");
}
// other code...
}

bUseDebugCRT(bool)

用来控制输出的 Runtime Librart 类型是 MT 还是 MD
还用来控制添加 _DEBUGNODEBUG宏:

1
2
3
4
5
6
7
8
9
10
11
12
public override void SetUpConfigurationEnvironment(ReadOnlyTargetRules Target, CppCompileEnvironment GlobalCompileEnvironment, LinkEnvironment GlobalLinkEnvironment)
{
if (GlobalCompileEnvironment.bUseDebugCRT)
{
GlobalCompileEnvironment.Definitions.Add("_DEBUG=1"); // the engine doesn't use this, but lots of 3rd party stuff does
}
else
{
GlobalCompileEnvironment.Definitions.Add("NDEBUG=1"); // the engine doesn't use this, but lots of 3rd party stuff does
}
// other code
}

ProjectDefinitions(List<string>)

为当前项目添加的宏定义,在整个项目中可用。

GlobalDefinitions(List<string>)

添加在整个 Target 中都可以用的宏定义。

bShouldCompileAsDLL(bool)

将 Target 编译为 DLL,为 true 时要求 LinkTypeMonolithic

1
2
3
4
5
/// <summary>
/// Whether this target should be compiled as a DLL. Requires LinkType to be set to TargetLinkType.Monolithic.
/// </summary>
[RequiresUniqueBuildEnvironment]
public bool bShouldCompileAsDLL = false;

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
2
3
4
5
6
7
8
9
10
11
using UnrealBuildTool;
using System.IO;

public class GWorld : ModuleRules
{
public GWorld(ReadOnlyTargetRules ReadOnlyTargetRules) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
// something
}
}

通过 Target 对象,可以在 *.build.cs 中控制对不同的平台(Platform),架构(Architecture),以及其他的选项来对 Module 进行不同的操作(比如定义不同的宏 / 包含不同的 ThridParty/ 链接不同的 Lib 等等)。

ModuleDirectory

  • string ModuleDirectory:项目的源码路径 PROJECT_NAME/Source/PROJECT_NAME 的绝对路径。

EngineDirectory

  • string EngineDirectory:引擎目录 Engine/ 在当前环境的绝对路径。

PublicAdditionalLibraries

添加静态链接库文件 (注意与PublicLibraryPaths 的区别),一般是用于第三方库的链接。

1
2
3
4
5
6
7
8
PublicAdditionalLibraries.AddRange(
new string[]
{
Path.Combine(ThridPartyPath,"protobuf/lib/Win64/MD/Release","libprotobuf.lib"),
Path.Combine(ThridPartyPath,"protobuf/lib/Win64/MD/Release","libprotobuf-lite.lib"),
Path.Combine(ThridPartyPath,"protobuf/lib/Win64/MD/Release","libprotoc.lib"),
}
);

详细的内容可以看:Linking Static Libraries Using The Build System

同样可以用在 DLL 的导入库,与 PublicDelayLoadDLLsRuntimeDependencies配合使用。

PublicAdditionalShadowFiles

当执行远程编译的时候,指定当前模块需要复制到远程服务器上的文件,确保能够链接成功。

如远程打包 IOS 平台时,需要把当前模块依赖的静态链接库添加到里面(如 Game 模块依赖某个插件中的 External 模块)。

RuntimeDependencies

  • list<RuntimeDependency> RuntimeDependencies:Module 在运行时依赖的文件 (.so/.dll 等),打包时将会拷贝到存储目录。

在打包 Windows 时会直接把文件拷贝到打包的对应目录下,但是在 Android 上会把文件放到 Apk 包的 main.obb.webp 中。

PublicDelayLoadDLLs

  • List<string> PublicDelayLoadDLLs:延迟加载的 DLL 列表,通常用于第三方库。
1
2
3
// build.cs
PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath, "myExternalLib.lib"));
PublicDelayLoadDLLs.Add("myExternalLib.dll");

含义是不在程序启动时立即加载 DLL 的列表,等到首次需要使用他们的符号后再进行加载。这样可以在模块的 StartupModule 中自行指定位置并加载他们,从而实现可以不把 dll 放到 exe 的目录。

1
2
3
4
5
6
FString AbsPath = FileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*MyLibPath);
FPlatformProcess::AddDllDirectory(AbsPath)
FPlatformProcess::PushDllDirectory(*AbsPath);
// direct call dll function
// or load dll handle
// void* DLLHandle3 = FPlatformProcess::GetDllHandle(L"myExternalLib.dll");

PS:配合 PublicAdditionalLibraries 可以用在使用 DLL 导入库的第三方库。

  1. 使用 PublicAdditionalLibraries 添加 lib
  2. DLL 的名字添加至 PublicDelayLoadDLLs
  3. 使用 RuntimeDependencies 打包时拷贝 dll
  4. 如果拷贝到的目录不是 exe 路径,需要 StartupModule 里执行 AddDllDirectoryPushDllDirectory把 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
2
3
4
5
PublicSystemIncludePaths.AddRange(
new string[] {
"TEST_LIB"
}
);

它表示的路径是:

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

因为动态链接库的查找路径默认只有:

  1. 系统的 PATH 路径;
  2. 可执行程序的当前目录;

如果我们的动态链接库在其他的位置,运行时就会错误,可以通过 PublicRuntimeLibraryPaths 或者 PrivateRuntimeLibraryPaths 来添加。

PublicLibraryPaths

添加链接库文件的 路径,如在源码中使用的:

1
#pragma comment(lib,"Lua.lib")

可以通过 PublicLibraryPaths 来添加依赖的 Lib。

DynamicallyLoadedModuleNames

  • List<string> DynamicallyLoadedModuleNames:添加需要运行时动态加载的 Module,使用 FModuleManager::LoadModuleChecked<MODULE_TYPE>(TEXT("MODULE_NAME")) 等函数启动。
1
2
// e.g
FModuleManager::LoadModuleChecked< IAIModule >("AIModule" );

PublicDependencyModuleNames

  • List<string> PublicDependencyModuleNames:添加对执行 Module 的源文件依赖,自动添加所依赖 Module 的 PublicPrivate源文件包含。

PrivateDependencyModuleNames

  • List<string> PrivateDependencyModuleNames:与 PublicDependencyModuleNames 不同的是,意味着所依赖的 Module 中的源文件只可以在 Private 中使用。

假如现在有一个模块 A,还有一个模块 B,他们中都是 UE 的 Module/PublicModule/Private的文件结构。

  • 如果 B 中依赖 A,如果使用的是 PrivateDependencyModuleNames 的方式添加的依赖,则 A 模块的源文件只可以在 B 的 Private 目录下的源文件中使用,在 Public 目录下的源文件使用时会报 No such file or directory 的错误。
  • 如果使用的是 PublicDependencyModuleNames 方式添加的依赖,则 A 的源文件在 B 的 PublicPrivate中都可用。

除了上述的区别之外,还影响依赖于 B 模块的模块 ,当一个模块 C 依赖模块 B 的时候,只能访问到 B 模块的 PublicDependencyModule 中的模块暴露出来的类。
例如,C 依赖 B,B 依赖 A;那么,假如 C 想访问 A 中的类则有两种方式:

  1. 在 C 的依赖中添加上 A 模块
  2. 确保 B 在 PublicDependencyModuleNames 依赖中添加的 A 模块,这样 C 就可以间接的访问到 A。

经过测试发现,其实对于 游戏模块 (PROJECT_NAME/Source/PROJECT_NAME.target.cs) 使用而言,所依赖的模块是使用 PublicDependencyModuleNames 还是 PrivateDependencyModuleNames 包含,没什么区别。
使用 Private 方式依赖的 Module 中的头文件依然可以在游戏模块的 Public 中用,这一点与插件等其他模块有所不同(但是这只有在所依赖的模块不是 bUsePreCompiled 的基础上的,如果所依赖的模块是 bUsePreCompiled 的,则与其他的模块一样,PrivateDependencyModuleNames依赖的模块不可以在 Pulibc 目录下的源文件使用),这个行为比较奇怪:有时候出错有时又不出错。

注意:在游戏项目中使用依赖其他 Module 时尽量确定性需求地使用 PrivateDependencyModuleNames 或者PublicDependencyModuleNames,在组合其他的选项时可能会有一些奇怪的行为。

相关的讨论:

  1. What is the difference between PublicDependencyModuleNames and PrivateDependencyModuleNames
  2. Explanation of Source Code folder structure?

bPreCompile 与 bUsePreCompiled

1
2
3
4
5
6
7
8
9
/// <summary>
/// Whether this module should be precompiled. Defaults to the bPrecompile flag from the target. Clear this flag to prevent a module being precompiled.
/// </summary>
public bool bPrecompile;

/// <summary>
/// Whether this module should use precompiled data. Always true for modules created from installed assemblies.
/// </summary>
public bool bUsePrecompiled;

这个两个属性需要组合来使用。

考虑下列需求:
如果我们写好的一个模块 A 希望拿给别人来用,但是又不想把所有代码开放出来,该怎么办?

在传统的 C++ 领域,我应该会说:把代码编译成 DLL,然后把头文件和 DLL 发放给用户就可以啦。
对!其实 bPreCompilebUsePreCompiled就是做的类似的事情。

当我们对模块 A 进行编译之前,在它的 *.build.cs 中添加:

1
2
3
4
5
6
7
8
9
public class A : ModuleRules
{
public A(ReadOnlyTargetRules Target) : base(Target)
{
// ...
bPreCompile=true;
// ...
}
}

然后编译模块 A。编译完成之后,将模块 A 的 Source/Private 删除 (删除之前请确保你已经备份),然后删除模块目录下的Intermediate,但是要保留Binaries 目录。
最后,打开模块 A 的 A.build.cs,将bPreCompile=true; 删掉,然后再添加:

1
2
3
4
5
6
7
8
9
public class A : ModuleRules
{
public A(ReadOnlyTargetRules Target) : base(Target)
{
// ...
bUsePreCompiled=true;
// ...
}
}

此时我们想要实现的目标都已经完成了:不发布实现代码(Private),发布预先编译好的二进制,但是这样无法进行静态链接,如果只是暴露给蓝图使用可以,在其他的 Module 中使用它的符号会有符号未定义错误。

OptimizeCode(CodeOptimization)

这个属性是用来控制当前模块是否要开启优化代码,在我们用 VS 调试时,有时候会看到“变量已被优化,因而不可用”,这就是因为被优化了。

可以使用它来关闭优化:

1
2
// build.cs
OptimizeCode = CodeOptimization.Never;

CodeOptimization支持几种值,默认是Default,开启优化:

  • Never
  • Default
  • InNonDebugBuilds
  • InShippingBuildsOnly

相关的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// UnrealBuildTool/Configutation/UEBuildModuleCPP.cs
public static bool ShouldEnableOptimization(ModuleRules.CodeOptimization Setting, UnrealTargetConfiguration Configuration, bool bIsEngineModule)
{
switch(Setting)
{
case ModuleRules.CodeOptimization.Never:
return false;
case ModuleRules.CodeOptimization.Default:
case ModuleRules.CodeOptimization.InNonDebugBuilds:
return (Configuration == UnrealTargetConfiguration.Debug)? false : (Configuration != UnrealTargetConfiguration.DebugGame || bIsEngineModule);
case ModuleRules.CodeOptimization.InShippingBuildsOnly:
return (Configuration == UnrealTargetConfiguration.Shipping);
default:
return true;
}
}

这个函数在 UEBuildModuleCPP.csCreateModuleCompileEnvironment中调用,将结果赋值给了 CppCompileEnvironment.bOptimizeCode,进而又在VCToolChain.cs 中被使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
UnrealBuildTool\Platform\Windows\VCToolChain.cs

void AppendCLArguments_Global(CppCompileEnvironment CompileEnvironment, List<string> Arguments)
{
// other code ...

//
// Debug
//
if (CompileEnvironment.Configuration == CppConfiguration.Debug)
{
// Disable compiler optimization.
Arguments.Add("/Od");

// Favor code size (especially useful for embedded platforms).
Arguments.Add("/Os");

// Allow inline method expansion unless E&C support is requested
if (!CompileEnvironment.bSupportEditAndContinue && CompileEnvironment.bUseInlining)
{
Arguments.Add("/Ob2");
}

if ((CompileEnvironment.Platform == CppPlatform.Win32) ||
(CompileEnvironment.Platform == CppPlatform.Win64))
{
Arguments.Add("/RTCs");
}
}
//
// Development and LTCG
//
else
{
if(!CompileEnvironment.bOptimizeCode)
{
// Disable compiler optimization.
Arguments.Add("/Od");
}
else
{
// Maximum optimizations.
Arguments.Add("/Ox");

// Favor code speed.
Arguments.Add("/Ot");

// Coalesce duplicate strings
Arguments.Add("/GF");

// Only omit frame pointers on the PC (which is implied by /Ox) if wanted.
if (CompileEnvironment.bOmitFramePointers == false
&& ((CompileEnvironment.Platform == CppPlatform.Win32) ||
(CompileEnvironment.Platform == CppPlatform.Win64)))
{
Arguments.Add("/Oy-");
}
}

// Allow inline method expansion
Arguments.Add("/Ob2");

//
// LTCG
//
if (CompileEnvironment.bAllowLTCG)
{
// Enable link-time code generation.
Arguments.Add("/GL");
}
}

// other code...
}

可以看到,在 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
#if GOOGLE_PROTOBUF_USE_UNALIGNED

如果这个宏未定义,在启用 bEnableUndefinedIdentifierWarnings 的情况下会产生 C4688 错误。

相关的代码时定义在 UBT 的代码中的:

1
2
3
4
5
6
7
8
9
10
11
12
// Source\Programs\UnrealBuildTool\Platform\Windows\VCToolChain.cs
if(WindowsPlatform.bUseVCCompilerArgs && CompileEnvironment.bEnableUndefinedIdentifierWarnings)
{
if (CompileEnvironment.bUndefinedIdentifierWarningsAsErrors)
{
Arguments.Add("/we4668");
}
else
{
Arguments.Add("/w44668");
}
}

bUseRTTI (bool)

UE4 默认关闭了 RTTI,所以在工程的代码中写了类似 typeid 的代码,会产生下列错误:

1
2
3
In file included from C:\UnrealProject_\Source\GWorld\Private\Modules\Flibs\FLibIniConfigHelper.cpp:10:
C:/UnrealProject_/Source/GWorld/Public/Modules/Flibs\FlibMateReflectionHelper.h(29,41): error: cannot use typeid with -fno-rtti
FString EnumTypeName = ANSI_TO_TCHAR(typeid(ENUM_TYPE).name());

解决办法只有两个:去掉 rtti 相关的代码,或者在当前 Modulebuild.cs中把 bUseRTTI 设置为true