反射是程序在運行時進行自檢的一種能力。它非常有用且在虛幻引擎中基礎技術,支撐了諸如 編輯器中的細節面板、序列化、垃圾回收、網絡復制、以及藍圖與C++交互等功能。然而,C++原生並不支持任意形式的反射,因此 虛幻引擎有它自己的系統用來 利用、查詢以及操作關於C++類、結構體、函數 、成員變量以及枚舉的信息。我們特意把反射叫做屬性系統,因為反射也是一個圖形術語。
反射系統是可以選擇加入的。你需要給暴露給反射系統的類型或屬性添加注解,這樣Unreal Header Tool (UHT)就會在編譯工程的時候利用那些信息生成特定的代碼。
標記
為了標記一個頭文件包含反射類型,需要在文件頂部添加一個特殊的include文件。這讓UHT知道它需要考慮這個文件,並且在反射系統的實現里也是需要的。
#include "FileName.generated.h"
你現在可以使用UENUM()、UCLASS()、USTRUCT()、UFUNCTION()、以及UPROPERTY()來在頭文件中注解不同的類型以及成員變量。每一個宏都會出現在類型或者成員變量的前面,並且可以包含額外的修飾符關鍵字。讓我們來看一個真實的例子(從StrategyGame截取一部分):
////////////////////////////////////////////////////////////////////////// // Base class for mobile units (soldiers) #include "StrategyTypes.h" #include "StrategyChar.generated.h" UCLASS(Abstract) class AStrategyChar : public ACharacter, public IStrategyTeamInterface { GENERATED_UCLASS_BODY() /** How many resources this pawn is worth when it dies. */ UPROPERTY(EditAnywhere, Category=Pawn) int32 ResourcesToGather; /** set attachment for weapon slot */ UFUNCTION(BlueprintCallable, Category=Attachment) void SetWeaponAttachment(class UStrategyAttachment* Weapon); UFUNCTION(BlueprintCallable, Category=Attachment) bool IsWeaponAttached(); protected: /** melee anim */ UPROPERTY(EditDefaultsOnly, Category=Pawn) UAnimMontage* MeleeAnim; /** Armor attachment slot */ UPROPERTY() UStrategyAttachment* ArmorSlot; /** team number */ uint8 MyTeamNum; [more code omitted] };
這個頭文件聲明了一個繼承自ACharacter叫做AStrategyChar的類。它使用UCLASS()來表明它是需要反射的類型,在C++定義內部它還包含一個GENERATED_UCLASS_BODY()的宏。GENERATED_UCLASS_BODY() / GENERATED_USTRUCT_BODY()在需要反射的類或者結構體里面是必要的,因為它們會加入額外的函數和typedef到類的內部。
第一個顯示的屬性是ResourcesToGather,它包含了EditAnywhere和Category=Pawn的注解。這表示這個 屬性可以在編輯中的任意細節面板中進行編輯,並且會顯示在Pawn分類中。有幾個以BlueprintCallable以及分類作為注解的函數,這表示它們可以在藍圖里面調用到這些C++函數。
由MyTeamNum的定義所示,在同一個類里面混合使用需要反射的屬性以及不需要反射的屬性是允許的,你只需要記住非反射的屬性對所有依賴反射功能的系統都是不可見的(例如存儲一個非反射的UObject裸指針是很危險的,因為垃圾回收系統不知道你在引用着它。)
每一個修飾符(例如EdityAnywhere、BlueprintCallabe)在ObjectBase.h中均有定義,並且都帶有一個簡短的注釋來說明它的意義或者用途。如果你不清楚一個關鍵字是干什么的,Alt+G一般會把你帶到ObjectBase.h的定義處。
查看游戲性編程參考來獲取更多信息。
限制
UHT並不是一個真正的C++分析器,它只能理解這個語言的一個子集並且會嘗試跳過那些它不需要的文本。只關注那些跟反射類型、函數以及屬性。然而,一些用法仍然會迷惑它,因此當你需要添加 一個反射類型到一個頭文件時,你可能 需要改寫一些代碼或者把它們用#if CPP / #endif。你也應該避免使用#if /#endif(除了WITH_EDITOR和WITH_EDITORONLY_DATA)把注解的屬性或者函數包含起來,因為生成的代碼會引用 它們並且會在任意define為假的工程配置中造成編譯錯誤。
大多婁的通用類型會正常得工作,但是屬性系統並不能表示所有可能的C++類型(只支持一些如TArray和TSubclassOf的模板類型,並且它們的模板類型不能是嵌套的類型)。如果你注解了一個不同表示的類型,那么UHT在運行時會給出一個比較詳細的錯誤描述。
使用反射數據
大多數的游戲代碼可以在運行時忽略屬性系統,也可以享受它給你帶來的好處,但是當你在寫工具代碼或者構建游戲性系統的時候,你就會覺得它很有用了。
屬性系統的類型層次大約如下所示:
UStruct是所有 聚合結構體的基礎類型(包含其它成員的類型,比如一個C++類、結構體、或者函數),不應該跟C++中的結構體(struct)混為一談(那是UScriptStruct)。UClass可以包含函數、屬性以及它們的孩子,而UFunction和UStriptStruct只能包含屬性。
你可以通過使用UTypeName::StaticClass()或者FTypeName::StaticStruct()來獲取反射類型對應的UClass以及UScriptStruct,你也通過 一個UObject的實例通過Instance->GetClass()來獲取類型(不能通過一個結構體實例的獲取類型,因為結構體沒有一個通用的基類或者需要的存儲空間)。
要想迭代一個UStruct的所有成員,可以使用TFieldIterator:
for (TFieldIterator<UProperty> PropIt(GetClass()); PropIt; ++PropIt) { UProperty* Property = *PropIt; // Do something with the property }
TFieldIterator的模板參數作為一個過濾器(因此你可以通過使用UField查看所有屬性和函數,或者僅查看其中的一個)。迭代器構造函數 的第二個參數是用來 表示 你是否只需要這個指定的類或結構體引入的項或者包括它父類/結構體(默認值 )。它對函數沒有任何效果。
每一個類型都有一系列標記(EClassFlags + HasAnyClassFlag等),並且包含一個繼承自UField的元數據(metadata)存儲系統 。關鍵字修飾符存儲為標記或者元數據,取決於它們是否游戲在運行時需要,或者只是 作為編輯器的功能。這允許只對編輯器有用的元數據可以去掉用來 節省內存,而運行時的標記卻一直有效。
你可以利用反射數據來做很多不同的事情 (枚舉屬性,以數據驅動的方式來獲取、設置值,調用反射函數,甚至是創建新的對象)與其深入這里說的某個事例,倒不如看看UnrealType.h和Class.h中的代碼,並且研究其中的一個與你想要完成功能相似的代碼示例。
反射實現機制簡要說明
如果你僅僅是想用反射系統,那么你可以路過這個章節,但是了解它是如何工作的能幫助你激發一些決策和在包含反射系統的頭文件中限制。
Unreal Build Tool(UBT)和Unreal Header Tool (UHT)兩個協同工作來生成運行時反射需要的數據。UBT屬性通過掃描頭文件,記錄任何至少有一個反射類型的頭文件的模塊。如果其中任意一個頭文件從上一次編譯起發生了變化,那么 UHT就會被調用來利用和更新反射數據。UHT分析頭文件,創建一系列反射數據,並且生成包含反射數據的C++代碼(放到每一個模塊的moulde.generated.inl中。注:最新版會生成到moudle.generated.cpp中),還有各種幫助函數以及thunk函數(每一個 頭文件 .generated.h)
用生成的C++代碼來存儲反射數據的一個最大好處就是,它可以保證跟二進制做到同步。你永遠也不會加載陳舊或者過時的反射數據,因為它是跟引擎的其它代碼同時編譯的,並且它會在程序啟動的時候使用C++表達式來計算成員偏移等,而不是通過針對特定平台/編譯器/優化的組合中進行逆向工程。UHT作為一個單獨的不使用任何生成頭文件的程序來構建,因此它也避免了雞生蛋、蛋生雞的問題,這個在虛幻3的腳本編譯器中一直被詬病。
生成的諸如StaticClass()、StaticStruct()函數是為了讓當前類型更好的獲取反射數據,以及那此轉換函數(thunks)用來在藍圖或者網絡復制中調用C++函數。這些必須聲明為類或者結構體的一部分,這也就解釋了為什么GENERATED_UCLASS_BODY() or GENERATED_USTRUCT_BODY()宏會包含在你的反射系統的類型中,而#include "TypeName.generated.h"的頭文件中定義了這些宏。