探讨UE4中的UBT和UHT
前言
UBT和UHT是编译工具,谁定义的呢,虚幻引擎自己定义的,拿来做什么呢,UBT和UHT是UE4用来简化多平台编译,去除用户自定义平台编译项目的操作
我们写的UE4代码不是标准的C++代码,是基于UE4源代码层层改装了很多层的,UHT将UE4代码转换成标准的C++代码,而UBT负责调用UHT来实现这个转化工作,转化完之后UBT调用标准的C++代码的编译器来将UHT转化后的标准C++代码完成编译成二进制文件,整体上看,UHT是UBT的编译流程的一部分
UBT
UBT:Unreal Build Tool
Unreal Build Tool由C#编写,且作为整个虚幻编译过程中第一个编译步骤,当你运行"GenerateProjectFiles"(一个批处理文件,用于Window平台下生成Visual Studio的解决方案和工程),第一个步骤就是在Source/Programs/UnrealBuildTool/UnrealBuiltTool.csproj工程下执行MSBuild来编译这个"Unreal Build Tool",所以可以理解UBT其实就是一个命令行程序,却可以完成很多事情,比如生成工程文件、执行UBT、为各种不同的平台构建风格来调用编译器(Compiler)和链接器(Linker)


接下来深入了解下UBT的几个方面:Target
、Modules
、BuildConfigration
、IWYU
Target
Target是通过C#源文件声明的,扩展名为.target.cs,并存储在项目的Source目录下。每个.target.cs文件都声明一个类,从TargetRules基类衍生而来
类的名称必须与在其中声明这个类的文件的名称相匹配,后跟"Target"(MyProject.target.cs定义类"MyProjectTarget")
Modules
模块是UE4的构建模块。引擎是由大量模块集合实现的,开发游戏的时候提供自己的模块来进行扩充,每个模块都包含了一组功能,并且可以提供公共接口和编译环境(包括宏、路径等)来让其他模块进行使用。(如.Build.cs文件中的PublicIncludePaths、PrivateIncludePaths、PublicDependencyModuleNames、PrivateDependencyModuleNames、DynamicallyLoadedModuleNames)
模块是通过C#源代码声明的,扩展名为.build.cs,存储在项目的Source目录下。属于一个模块的C++源代码与.build.cs文件并列存储(Private和Public一般分别存放.h和.cpp)
一个标准模块ModuleA.build.cs的内容

这些build.cs文件都由UBT编译,并被构造来确定整个编译环境
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class ModuleA : ModuleRules
{
public ModuleA(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.AddRange(
new string[] {
// ... add public include paths required here ...
}
);
PrivateIncludePaths.AddRange(
new string[] {
// ... add other private include paths required here ...
}
);
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
// ... add other public dependencies that you statically link with here ...
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"Projects",
"InputCore",
"UnrealEd",
"ToolMenus",
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"DesktopPlatform",
"EditorStyle",
// ... add private dependencies that you statically link with here ...
}
);
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
// ... add any modules that your module loads dynamically here ...
}
);
}
}
模块间不能循环引用,如在ModuleB.uplugin下添加中ModuleA
,此时ModuleB可以访问ModuleA.Build.cs文件中的PublicInclude中的头文件路径,ModuleA暴露这些头文件给ModuleB进行使用,但是如果此时ModuleA也想要使用ModuleB中的头文件呢,如果直接在Modules中进行添加,会编译错误,因为这会导致循环引用,此时需要将头文件中需要引入XXXX_API宏定义才可以暴露给其他模块进行使用

BuildConfiguration
除了添加到Config/UnrealBuildTool文件夹下生成的UE4项目之外,UnrealBuildTool还会从Windows上以下位置的XML配置文件读取设置:
- Engine/Saved/UnrealBuildTool/BuildConfiguration.xml
- User Folder/AppData/Roaming/Unreal Engine/UnrealBuildTool/BuildConfiguration.xml
- My Documents/Unreal Engine/UnrealBuildTool/BuildConfiguration.xml

生成项目代码
首先,要做的是运行GenerateProjectFiles
- Unreal Build Tool被构建了
- 批处理文件调用了类似如下的命令(取决于您的项目,Visual Studio版本,平台等)
-ProjectFiles -nodummyconfigs -game -engine -2017 "-project=Path\To\Your\Project.uproject" -Platforms=Win64+XboxOne+UWP64 -noSolutionSuffix - Unreal Build Tool会在引擎和游戏目录搜索所有带有.Build.cs拓展的文件来发现所有定义的模块
- Unreal Build Tool会搜索所有带有.Target.cs拓展的文件来发现所有定义的目标
- 它将生成一个包含所有目标作为构建配置和所有模块作为项目的解决方案
C#项目只是源文件夹中的.csproj文件。C ++项目并不完全是“标准”项目。它不再调用MSBuild,而是调用UnrealBuildTool
构建C++项目
在Unreal中构建C++项目时,您可以看到(基于vcxproj的NMakeBuildCommandLine属性)将调用与此类似的命令行
C:\Path\To\Your\Engine\Build.bat TargetName Win64 Debug "$(SolutionDir)$(ProjectName).uproject" -waitmutex $(AdditionalBuildArguments) -2017
它的背后其实又调用了UnrealBuildTool
它的背后其实又调用了UnrealBuildTool
那么,UnrealBuildTool在这儿的作用是:
- 编译目标。它在运行时编译了.Target.cs代码(使用C#编译器)来获取构建属性。这是UnrealBuildTool从中获取大部分定义和平台信息的地方。某些属性(例如bBuildEditor)表示你需要的是构建编辑器。它会创建一个WITH_EDITOR定义,然后由编译器转发到源文件。以实现源代码中的条件编译:#if WITH_EDITOR 条件编译
- 解析所有依赖模块,包含来自.Target.cs和.Build.cs(模块)的依赖
- 将编译所有依赖模块的Build.cs,以获取有关如何构建每个模块的额外属性
- 解析哪些模块使用了共享编译头(即.Build.cs文件中包含SharedPCHHeaderFile属性,比如CoreUObject,Core,Engine等)
- 解析哪些模块依赖于UObject模块
- 对所有依赖于UObject的模块运行Unreal Header Tool,这时虚幻引擎会注入一些行为到你的类中,强制你在文件中加入由Unreal Header Tool生成的“.generated.h”头文件
- 基于Unreal Header Tool生成的代码,解析所有Include路径
- 基于解析后的路径、定义、外部库等,生成一系列会在目标环境执行的命令列表
- 为共享预编译头调用编译器(CL.EXE)
- 调用编译器来编译源文件(CL.EXE)
- 调用链接器(LINK.EXE)
- 调用所有这些操作
反射机制
反射在Java和C#等语言中比较常见,概况的说,反射数据描述了类在运行时的内容。这些数据所存储的信息包括类的名称、类中的数据成员、每个数据成员的类型、每个成员位于对象内存映像的偏移,此外,它也包含类的所以成员函数信息。
C++本身不支持反射,Unreal engine在C++基础上搭建了自己的一套反射机制。具体来看,对于一个类(UClass),我们可以获得这个类的所有属性和方法,而对于一个类对象,我们可以调用它所拥有的方法和属性,前提是这些属性和方法被纳入到UE4的反射系统。
虚幻4使用反射可以实现序列化、editor的details panel、垃圾回收、网络复制、蓝图/C++通信、相互调用、蓝图结构体导出JSON文件、把JSON文件写入到类的结构体变量、修改和读取任意UPROPERTY宏标记的变量数据等功能。
反射系统是选择加入的,只有主动标记的类型、属性、方法会被反射系统追踪,Unreal Header Tool会收集这些信息,生成用于支持反射机制的C++代码,然后再编译工程。
UHT
UHT:Unreal Header Tool
Unreal engine背后强大的反射机制离不开UHT
UENUM()、UCLASS()、USTRUCT()、UFUNCTION()、UPROPERTY()来标记不同的类型和成员变量
也可以标记一个含有反射类型的头文件,需要添加一个特殊的#include
#include "FileName.generated.h"
将反射数据保存为C++代码的一个好处为可以与二进制文件保持一致,永远不会加载过期的反射数据,因为它们参与编译,永远也不会加载陈旧或过时的反射数据。UHT被设计为一个独立的程序,自己本身不使用任何的generated headers,因此避免了先有鸡还是先有蛋的问题。
.generated.h中生成的函数包含了XXX_Implementation之类的补全,也包含了用于蓝图中调用C++函数的转换函数,并通过GENERATED_BODY()安插到我们编写的类中。
注意:虽然UHT实现了近似C++解析器的功能,但毕竟只能理解一部分语法,不要用#if/#ifndef把标记抱起来,会出现错误。UE4提供了一些特殊的宏来兼容反射系统,比如WITH_EDIROT和WITH_EDITORONLY_DATA。
参考资料
https://ericlemes.com/2018/11/23/understanding-unreal-build-tool/
https://www.zhihu.com/search?type=content&q=unreal%20uht
https://zhuanlan.zhihu.com/p/57186557
本文作者:shadow_lr
本文链接:https://www.cnblogs.com/shadow-lr/p/UBT-UHT-InUE4.html
版权声明:本作品采用shadow-lr许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步