目录
0 引言
本篇文章对UE C++工程编译的总体流程做一个简单的梳理。
1 前置知识
1.1 模块
虚幻引擎(Unreal Engine, UE)的模块化系统是其核心架构之一,旨在提高引擎的可扩展性、代码的可维护性以及编译的效率。通过模块化设计,虚幻引擎将庞大的代码库划分为多个独立的模块,每个模块可以独立编译、加载和卸载。这种设计大大简化了大型项目的开发和维护。以下是对虚幻引擎模块化系统的详细解释。
1.1.1 模块的定义与结构
- 模块(Module) 是虚幻引擎中的一个基本构建单元,它通常由一组相关的源文件、头文件、资源文件等组成。每个模块可以看作是一个小型的子工程,具有自己的编译和链接规则。
- 目录结构:通常,一个模块的文件结构如下:
MyModule/ ├── MyModule.Build.cs ├── Public/ │ ├── MyClass.h ├── Private/ │ ├── MyClass.cpp ├── Resources/ │ ├── MyResource.uasset
Public/
目录包含模块对外公开的头文件。Private/
目录包含模块内部使用的源文件和头文件。Resources/
目录可以包含模块的资源文件。
1.1.2. 模块类型
虚幻引擎中的模块可以分为多种类型,每种类型适用于不同的场景:
- Runtime模块:用于在游戏运行时加载的模块,如渲染、物理、AI等模块。
- Editor模块:仅在编辑器中加载的模块,提供编辑器扩展功能。
- Developer模块:用于开发和调试工具,不会在最终的发布版本中包含。
- ThirdParty模块:集成第三方库或工具的模块。
- Program模块:独立的工具或程序模块,不作为引擎或游戏的一部分加载。
1.1.3. 模块的描述文件:Build.cs
每个模块都有一个.Build.cs
文件,这个文件是C#脚本,定义了模块的构建和配置规则。Build.cs
文件包含以下信息:
- 模块依赖:指定模块依赖的其他模块。例如,一个网络模块可能依赖于引擎的基础模块。
- 公共和私有依赖:区分模块的公共依赖(对外暴露)和私有依赖(仅模块内部使用)。
- 包含路径:指定公共和私有的头文件路径。
- 预编译头:指定模块使用的预编译头文件。
- 平台特定代码:在不同的平台上可以有不同的编译选项,例如在Windows、Mac、iOS上的不同设置。
- 编译选项:如编译标志、宏定义等。
示例Build.cs
文件:
using UnrealBuildTool; // 引入Unreal Build Tool的命名空间,用于定义模块的编译规则。
// 定义一个名为"MyModule"的模块,继承自"ModuleRules"类。
public class MyModule : ModuleRules
{
// 构造函数,接收一个只读的Target规则作为参数,初始化模块的编译规则。
public MyModule(ReadOnlyTargetRules Target) : base(Target)
{
// 设置PCH(预编译头文件)的使用模式。
// PCHUsageMode.UseExplicitOrSharedPCHs表示模块使用共享或显式的预编译头文件。
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
// 添加模块的公共依赖模块。
// 这些模块是"MyModule"模块所依赖的其他模块,并且它们的公共API对外可见。
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
// "Core":包含基本的低级引擎功能。
// "CoreUObject":包含对象系统和反射功能。
// "Engine":包含虚幻引擎的核心功能。
// "InputCore":处理输入设备(如键盘、鼠标)的模块。
// 添加模块的私有依赖模块。
// 这些模块是"MyModule"模块所依赖的其他模块,但它们的API仅在模块内部可见。
// 这里暂时没有添加私有依赖模块,因此保持为空。
PrivateDependencyModuleNames.AddRange(new string[] { });
// 设置公共头文件的包含路径。
// "PublicIncludePaths"中的路径将被添加到编译器的头文件搜索路径中,供其他模块引用。
PublicIncludePaths.AddRange(new string[] { "MyModule/Public" });
// 设置私有头文件的包含路径。
// "PrivateIncludePaths"中的路径仅供该模块内部使用,其他模块无法直接引用这些路径中的头文件。
PrivateIncludePaths.AddRange(new string[] { "MyModule/Private" });
}
}
1.1.4. 模块的编译与链接
- 独立编译:每个模块可以独立编译成目标文件或动态库,这样可以减少全量编译的时间。在项目修改时,只需要重新编译受影响的模块。
- 并行编译:虚幻引擎的构建工具(Unreal Build Tool, UBT)支持模块的并行编译,以充分利用多核CPU的性能,加快编译速度。
- 模块间的依赖管理:通过
Build.cs
文件中的依赖关系,UBT
可以自动处理模块间的链接顺序,确保在编译和链接时正确地解析依赖。
1.1.5. 模块的动态加载与卸载
- 动态加载:虚幻引擎支持在运行时按需加载模块。动态加载的模块通常是以动态链接库(DLL)或共享对象文件(SO)形式存在。通过这种机制,可以在游戏运行时添加或更新功能,而不需要重新启动游戏。
- 热重载(Hot Reload):在开发过程中,开发者可以修改代码并重新编译模块,而无需重启整个编辑器或游戏。虚幻引擎会卸载旧模块并加载新编译的模块,从而加快开发迭代速度。
1.1.6. 模块的依赖与插件
- 模块依赖:模块间的依赖关系由
Build.cs
文件中的PublicDependencyModuleNames
和PrivateDependencyModuleNames
指定。这些依赖关系决定了模块编译的顺序和链接时需要的库文件。 - 插件(Plugin):虚幻引擎的插件通常也是由一组模块组成。插件可以扩展引擎的功能,并且可以轻松地添加到项目中或从项目中移除。插件的模块可以是Runtime、Editor、Developer等类型。
1.1.7. 模块的优点
- 可扩展性:模块化设计允许开发者以独立的方式扩展引擎功能,降低了不同功能模块间的耦合度。
- 编译效率:通过独立编译和并行编译,模块化系统显著缩短了编译时间。
- 维护性:模块化代码更容易维护和调试,开发者可以专注于单个模块的开发,而无需处理整个工程的复杂性。
- 动态加载:支持按需加载功能,使得开发和运行时的资源管理更加灵活。
1.2 插件
在虚幻引擎(Unreal Engine,简称UE)中,插件(Plugins)是扩展引擎功能的一种强大机制。插件可以包含代码、资源、配置文件等,用于扩展和定制引擎的功能,而不需要修改引擎的核心代码。插件通常被用于添加新功能、集成第三方库或工具、开发自定义编辑器功能等。
1.2.1 UE插件的结构
每个插件通常包含以下几个部分:
-
uplugin
文件:*.uplugin
文件是插件的描述文件,使用JSON格式,定义了插件的基本信息,如名称、版本、作者、依赖模块、插件类型等。- 例如:
{ "FileVersion": 3, "Version": 1, "VersionName": "1.0", "FriendlyName": "My Plugin", "Description": "A simple plugin for Unreal Engine.", "Category": "Custom", "Modules": [ { "Name": "MyPlugin", "Type": "Runtime", "LoadingPhase": "Default" } ] }
-
源码文件:
- 插件可以包含C++源代码,通常组织在
Source
目录下。这个目录包含一个或多个模块的代码,每个模块有自己的.Build.cs
文件定义编译规则。 - 插件的源码可以是引擎模块、游戏模块,甚至是编辑器模块。
- 插件可以包含C++源代码,通常组织在
-
资源文件:
- 插件也可以包含资源文件,如材质、纹理、蓝图、音频文件等。通常存放在
Content
目录下。 - 这些资源可以与引擎项目中的资源一起使用。
- 插件也可以包含资源文件,如材质、纹理、蓝图、音频文件等。通常存放在
-
配置文件:
- 插件可以包含配置文件,用于定义编辑器设置、默认值等。配置文件通常使用
.ini
格式。
- 插件可以包含配置文件,用于定义编辑器设置、默认值等。配置文件通常使用
-
编辑器扩展:
- 插件可以包含编辑器扩展代码,比如自定义窗口、工具栏、菜单项等,提供额外的编辑器功能。
1.2.2 插件的类型
UE中的插件有几种主要类型,每种类型的插件在引擎中扮演不同的角色:
-
Runtime(运行时插件):
- 这些插件在游戏运行时加载,用于提供游戏逻辑、功能扩展等。Runtime插件通常用于添加与游戏相关的新特性或功能。
-
Editor(编辑器插件):
- 这些插件在引擎编辑器中使用,用于扩展编辑器功能,如自定义工具、窗口、资产管理等。Editor插件不包含在最终的游戏中,仅在开发阶段使用。
-
Development(开发插件):
- 用于开发和调试的插件,可以提供测试工具、性能分析工具等。类似于Editor插件,但通常在开发过程中使用。
-
Third Party(第三方插件):
- 这些插件通常用于集成第三方库或服务。它们可能提供引擎与外部系统或工具的接口。
-
Blueprint(蓝图插件):
- 这些插件通常用于扩展蓝图功能,可以添加新的节点、蓝图类或其他与蓝图相关的扩展。
1.3 ProjectName.uproject 和 PluginName.uplugin
.uproject
文件和 .uplugin
文件是 Unreal Engine 项目和插件的配置文件,它们在结构上类似,但用途不同,分别用于管理项目和插件的相关配置。以下是对这两个文件的详细解释:
.uproject
文件是 Unreal Engine 项目的配置文件,定义了项目的基本信息和依赖项。它的主要作用是告诉引擎如何加载项目,以及项目中使用了哪些插件和模块。
常见的 .uproject
文件内容:
- FileVersion: 文件版本,用于兼容性检查。
- EngineVersion: 指定项目使用的 Unreal Engine 版本。
- Modules: 列出项目中包含的模块。每个模块可以包含代码、资源等。
- Plugins: 列出项目中启用的插件,包含插件名称、是否启用等信息。
- TargetPlatforms: 指定项目支持的目标平台,如
Win64
,Android
,iOS
等。 - Description: 项目描述,用于提供有关项目的信息。
- AdditionalDependencies: 列出项目的额外依赖项,这些依赖项可能是其他插件或模块。
示例 .uproject
文件:
{
"FileVersion": 3,
"EngineVersion": "5.4.0",
"Modules": [
{
"Name": "MyGame",
"Type": "Runtime",
"LoadingPhase": "Default"
}
],
"Plugins": [
{
"Name": "MyCustomPlugin",
"Enabled": true
},
{
"Name": "OnlineSubsystem",
"Enabled": true
}
],
"TargetPlatforms": [
"Win64",
"Android"
]
}
.uplugin
文件是 Unreal Engine 插件的配置文件,定义了插件的基本信息、模块和依赖项。它用来告诉引擎如何加载和使用插件。
常见的 .uplugin
文件内容:
- FileVersion: 文件版本,用于兼容性检查。
- VersionName: 插件版本号,通常用于插件的版本管理。
- FriendlyName: 插件的友好名称,展示给用户。
- Description: 插件的描述信息。
- Category: 插件的类别,通常用于在插件市场或插件管理器中分类。
- Modules: 列出插件中包含的模块,指定模块类型(如
Runtime
,Editor
)和加载时机(如Default
,PostConfigInit
)。 - EnabledByDefault: 指示插件是否默认启用。
- CanContainContent: 指示插件是否可以包含内容(如资源、蓝图等)。
- EngineVersion: 指定插件适用的 Unreal Engine 版本。
- Plugins: 列出插件依赖的其他插件。
示例 .uplugin
文件:
{
"FileVersion": 3,
"VersionName": "1.0.0",
"FriendlyName": "MyCustomPlugin",
"Description": "A custom plugin for Unreal Engine.",
"Category": "Utility",
"Modules": [
{
"Name": "MyCustomModule",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "MyCustomEditorModule",
"Type": "Editor",
"LoadingPhase": "Default"
}
],
"EnabledByDefault": true,
"CanContainContent": true,
"EngineVersion": "5.4.0",
"Plugins": [
{
"Name": "SomeOtherPlugin",
"Enabled": true
}
]
}
区别和联系
-
用途:
.uproject
文件用于配置整个项目,包括项目的模块、使用的插件和目标平台等。.uplugin
文件用于配置插件,包括插件的模块、依赖项和其他相关信息。
-
管理对象:
.uproject
文件管理整个项目及其依赖的插件。.uplugin
文件管理插件及其包含的模块和依赖关系。
-
加载顺序:
- 在项目加载时,
Unreal Engine
首先会读取.uproject
文件,确定项目的插件依赖项并加载相应的插件。而在加载插件时,再根据.uplugin
文件中的配置加载插件的模块和资源。
- 在项目加载时,
通过这两个文件,Unreal Engine 可以灵活管理项目和插件,使得模块化开发和跨项目插件复用变得更加方便。
2 UE 工程编译的流程
在虚幻引擎(Unreal Engine)的项目中,由于工程规模庞大,整个编译过程确实与传统的C++项目有所不同。虚幻引擎通过一些特殊的技术和工具来加速编译过程并管理复杂的依赖关系。下面是虚幻引擎项目从CPP文件到二进制可执行文件的编译过程:
2.1 模块化系统
- 模块化编程:虚幻引擎采用了模块化的设计,每个模块(Module)可以独立编译和链接。模块化系统允许开发者将代码分解成多个模块,从而减少全量编译的时间。
- Build.cs文件:每个模块有一个
Build.cs
文件,指定该模块的依赖项、包含路径、宏定义等。这个文件在编译时由Unreal Build Tool (UBT)解析。
2.2 预处理
与传统C++项目类似,虚幻引擎项目也会执行预处理步骤(如宏展开、文件包含等)。UE5 在预处理阶段和传统 C++ 编译流程之间存在一些独特之处,主要体现在 Unreal Build Tool (UBT)
、反射系统
、以及模块化系统
的管理上。这些特点使得 UE5 更加适合大型游戏和复杂应用的开发。以下是 UE5 预处理与传统 C++ 的特别之处:
1 UBT的预处理管理
- 模块和插件系统: UBT 在编译开始前,会解析项目和插件的
.uproject
和.uplugin
文件,自动管理模块的依赖关系和加载顺序。与传统 C++ 直接调用编译器不同,UBT 通过一系列规则和配置文件来生成最终的编译命令。 - 预编译头文件(PCH)管理: UBT 在项目构建过程中会管理预编译头文件,优化编译速度。不同于传统 C++ 中手动设置 PCH 的方式,UBT 自动处理 PCH 的生成和使用。
2 反射系统
- UHT(Unreal Header Tool): UE5 使用 UHT 来处理反射系统相关的代码。UHT 在预处理阶段扫描代码中的 UCLASS、USTRUCT、UENUM、UFUNCTION 等宏,并生成相应的反射数据。这一额外的代码生成步骤是传统 C++ 所没有的。
- UProperties 的管理: 通过 UHT,UE5 能够为标记为
UPROPERTY
的成员变量生成额外的元数据,这使得这些属性能够在运行时被引擎访问和操作(如编辑器中的可视化、序列化等)。
3 代码生成
- 宏的复杂使用: UE5 中的大量反射宏和自定义宏对预处理器的依赖远超传统 C++。这些宏通常会触发 UHT 生成的额外代码,而在传统 C++ 中,预处理器主要用于条件编译和简单的代码替换。
- 扩展的类型系统: UE5 的类型系统通过反射和元数据扩展了传统 C++ 的能力。例如,UCLASS 类可以通过 UE4/UE5 提供的特定宏生成扩展功能,而传统 C++ 中没有这种机制。
4 自动化构建流程
- Target Rules 和 Module Rules: UBT 利用目标规则(Target Rules)和模块规则(Module Rules)文件来管理不同模块的编译选项。比如,某些模块可能指定使用不同的预处理模式、启用特定的优化选项或在不同平台下使用不同的编译器选项。传统 C++ 通常直接依赖于 Makefile 或 CMake 等工具手动配置编译选项,而 UBT 自动生成这些设置。
2.3 编译
- 编译步骤与传统C++项目类似,将CPP文件编译为目标文件(.obj或.o)。但由于模块化设计,虚幻引擎可以并行编译多个模块,进一步加速编译过程。
- PCH(预编译头):虚幻引擎使用预编译头文件(PCH)来加速编译。PCH包含了常用的头文件,这样可以避免在每次编译时重新解析这些头文件。
2.4 汇编
- 编译生成的目标文件会经过汇编器,生成机器代码。这一步骤与传统C++项目无异。
2.5 链接
- 所有生成的目标文件和库文件在此阶段被链接成最终的可执行文件或动态链接库(DLL)。
- 模块链接:虚幻引擎会将各个模块独立链接,生成相应的动态链接库,这样在运行时可以根据需要加载或卸载模块。
- 依赖管理:UBT在链接阶段会根据
Build.cs
文件中的依赖关系自动管理链接的顺序和所需的库文件。
2.6 热重载和动态加载
- Hot Reload:在开发过程中,虚幻引擎支持热重载功能,允许在不关闭编辑器的情况下重新编译和加载代码。
- 动态加载:引擎中使用动态加载(DLL或SO)的模块化设计允许在运行时按需加载模块,这对于大型工程的扩展和维护非常重要。
2.7 总结
虚幻引擎通过模块化系统、UHT工具、预编译头(PCH)和动态链接库(DLL)的使用,使得庞大的工程得以有效编译和管理。虽然大部分步骤与传统C++编译流程类似,但虚幻引擎的特殊处理确保了开发效率和灵活性,尤其是在大型项目中。。为什么UE5需要使用这种方式编译工程呢?