前言
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
