前言

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的幾個方面:TargetModulesBuildConfigrationIWYU

Target

Target是通過C#原始檔宣告的,副檔名為.target.cs,並存儲在專案的Source目錄下。每個.target.cs檔案都宣告一個類,從TargetRules基類衍生而來

類的名稱必須與在其中宣告這個類的檔案的名稱相匹配,後跟"Target"(MyProject.target.cs定義類"MyProjectTarget")

Modules

模組是UE4的構建模組。引擎是由大量模組集合實現的,開發遊戲的時候提供自己的模組來進行擴充,每個模組都包含了一組功能,並且可以提供公共介面和編譯環境(包括巨集、路徑等)來讓其他模組進行使用。(如.Build.cs檔案中的PublicIncludePathsPrivateIncludePathsPublicDependencyModuleNamesPrivateDependencyModuleNamesDynamicallyLoadedModuleNames

模組是通過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

https://docs.unrealengine.com/4.26/en-US/ProductionPipelines/BuildTools/UnrealBuildTool/BuildConfiguration/

生成專案程式碼

首先,要做的是執行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