虛幻引擎中的反射(譯)
反射是程式的一種能力,藉助於它可以在執行時檢視自身。作為虛幻引擎中的基礎技術,它相當有用,增強了眾多的系統比如編輯器中的屬性面板,物件序列化,垃圾回收,網路物件傳輸以及藍圖指令碼和C++之間的通訊等。不過C++語言本身並不提供任何形式的反射,因此虛幻引擎實現了一套自己的反射系統,通過它來收集,查詢和修改C++中的類,結構,函式,成員變數和列舉的資訊。在本文中我們提到反射通常是指屬性系統,而不是圖形學中的概念。
反射系統是可選的。如果你希望某些型別或者屬性對反射系統可見,那麼就必須給它們加上修飾巨集,這樣在編譯工程時Unreal Header Tool (UHT) 才會去收集這些資訊。
標示 為了標示一個頭檔案包含了反資料型別,我們需要在檔案的頭部包含一個特殊的檔案。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()來指定該類具有反射特性。與UCLASS()對應的,我們還在類定義內部插入了GENERATED_UCLASS_BODY() 巨集。對於想要加入反射的類和結構體中,GENERATED_UCLASS_BODY() / GENERATED_USTRUCT_BODY()是必須的。通過這兩個巨集,我們給類和結構體注入了實現反射所必須的額外函式和型別資訊。
在程式碼中,第一個反射屬性是ResourcesToGather。它被指定為EditAnywhere 和Category=Pawn。這意味著這個屬性可以在任意屬性面板裡編輯,並且屬於Pawn這個類別。此外好幾個函式指定了BlueprintCallable和一個類別,這意味這些函式都可以在Blueprints裡被呼叫。
如MyTeamNum宣告所示,在同一個類中混雜反射成員和非反射成員是沒有問題的,只是要注意的是非反射成員對於所有基於反射的系統來說都是不可見的(比如快取一個非反射的UObject指標通常來說是危險的,因為垃圾回收器並不知道你引用了它)。
你可以在ObjectBase.h裡找到每一個說明符關鍵字(比如EditAnywhere和BlueprintCallable)的一段簡短註釋和用法說明。如果你不知道某個關鍵字是起做什麼用的,按快捷鍵Alt+G會跳轉到ObjectBase.h中對應的定義上去了(這些不是真的C++關鍵字,但是智慧提示和VAX看起來不關心也分不清它們間的區別)。
更多的資訊可以參見官網上的Gameplay Programming Reference。
侷限
UHT並不是一個真正的C++解析器。它可以識別語言的常見子集,並且在解析時儘可能多的跳過任何它認為不相關的程式碼;同時僅關注反射的類,函式和屬性。儘管這樣,某些情況下它還是會出錯的,所以當往一個已有的標頭檔案里加上反射型別時,你可能要重寫一些程式碼,或者要把已有的程式碼包在#if CPP / #endif裡。你應該儘量避免把反射巨集修飾過的屬性或者函式包在 #if/#ifdef (WITH_EDITOR 和WITH_EDITORONLY_DATA除外)裡,這是因為在某些構建配置裡這些巨集定義有可能不是true的,那麼在生成的程式碼裡去引用這些屬性或者函式時就會報編譯錯誤了。
對於虛幻引擎的反射來說,C++中絕大多數的資料型別都是支援的,但是並不是所有(特別是只有少數模版型別是支援的,比如TArray和TSubclassOf,並且它們的模版引數不能是巢狀型別)。如果你使用反射巨集來修飾無法在執行時表示的資料型別,UHT會給你報一個描述性質的錯誤資訊。
使用反射資訊
雖然大部分的遊戲程式碼在執行時使用反射系統給予的便利的同時,可以忽略反射系統,但是當你編寫工具或者玩法系統時會發現反射還是很有用的。
反射系統的型別層級如下所示:
UField
UStruct
UClass (C++ class)
UScriptStruct (C++ struct)
UFunction (C++ function)
UEnum (C++ enumeration)
UProperty (C++ member variable or function parameter)
(更多不同型別的子類)
UStruct是聚合類結構(任何包含了其他成員的型別,比如C++類,結構,或者函式)的基礎型別,不要把它和C++的結構混淆起來(與它對應的是UScriptStruct)。UClass可以包含函式或者屬性作為它的成員,但UFunction和UScriptStruct只能侷限於屬性。
通過UTypeName::StaticClass() 或者 FTypeName::StaticStruct(),你可以獲取某個反射C++型別的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 + HasAnyClassFlags, etc…)和一個繼承自UField的通用元資料儲存系統。反射巨集中的說明符關鍵字可以作為標誌位儲存也可以作為元資料儲存,取決於它們是被用於遊戲執行時,還是隻是在編輯器裡。這樣就可以實現在遊戲執行時去除僅在編輯器用的元資料來達到節省記憶體的目的,而作為標誌位儲存的則一直可用。
你可以通過應用反射資料來實現不同的功能(比如列舉屬性,按資料驅動的方式獲取/設定屬性,呼叫反射方法,甚至建立新例項);與其深入瞭解每個反射特性,不如瀏覽一下UnrealType.h 和 Class.h,然後跟蹤除錯一個和你要做的功能相近的樣例來得更容易一些。
反射的實現原理
如果你僅僅只是想使用反射系統而已,那麼可以毫不猶豫的跳過這一部分;但是知道它是怎麼工作的可以讓你在使用時更好的做出決定並瞭解它的侷限性。
Unreal Build Tool (UBT) 和 Unreal Header Tool (UHT)在實現執行時的反射功能中扮演著核心的角色。UBT的工作就是掃描標頭檔案,如果一個頭檔案包含至少一個反射型別則記錄該標頭檔案所在的模組。如果這些標頭檔案在編譯之後發生改變,UHT就會被喚起收集並更新對應的反射資料。UHT解析標頭檔案,建立反射資料集合,然後生成包含反射資料(包含在每個模組都有的.generated.inl裡)以及各類輔助類和函式(包含在每個標頭檔案對應的.generated.h裡)的C++程式碼。
之所以通過生成的C++程式碼來儲存反射資訊,一個主要的好處是這樣可以確保這些資訊和最終的二進位制檔案保持同步。把反射資訊和引擎程式碼一起編譯,並在啟動時通過C++表示式來計算成員的偏移等資訊,而不是逆向工程某個特定的平臺/編譯器/優化選項的組合,這樣你永遠都不會載入到錯誤的反射資訊。UHT作為一個獨立的程式,它不會修改任何生成的標頭檔案,這樣就避免了在UE3指令碼編譯中經常被抱怨的先有蛋還是先有雞這樣的問題。
生成的方法包括像StaticClass() / StaticStruct(),方便你獲得某個型別反射資料的型別,生成的程式碼段則方便你在Blueprints或者網路傳輸中呼叫。這些東西必須作為類或者結構體的一部分來宣告,這就是為什麼GENERATED_UCLASS_BODY() 或GENERATED_USTRUCT_BODY()巨集要包含在反射型別裡,以及標頭檔案中需加入包含定義這些巨集的程式碼#include “TypeName.generated.h” 的原因。