原文鏈接:https://www.unrealengine.com/en-US/blog/unreal-property-system-reflection?sessionInvalidated=true
反射是程序的一種能力斜棚,借助于它可以在運行時查看自身。作為虛幻引擎中的基礎技術兽狭,它相當有用抛猖,增強了眾多的系統(tǒng)比如編輯器中的屬性面板,對象序列化救赐,垃圾回收智政,網(wǎng)絡對象傳輸以及藍圖腳本和C++之間的通信等掏缎。不過C++語言本身并不提供任何形式的反射,因此虛幻引擎實現(xiàn)了一套自己的反射系統(tǒng)衫冻,通過它來收集诀紊,查詢和修改C++中的類,結構隅俘,函數(shù)邻奠,成員變量和枚舉的信息。在本文中我們提到反射通常是指屬性系統(tǒng)为居,而不是圖形學中的概念碌宴。
反射系統(tǒng)是可選的。如果你希望某些類型或者屬性對反射系統(tǒng)可見蒙畴,那么就必須給它們加上修飾宏贰镣,這樣在編譯工程時Unreal Header Tool (UHT) 才會去收集這些信息呜象。
標示
為了標示一個頭文件包含了反數(shù)據(jù)類型,我們需要在文件的頭部包含一個特殊的文件碑隆。UHT會識別出這個文件需要處理恭陡,并且也會為該頭文件加上反射系統(tǒng)的實現(xiàn)代碼(更多的介紹請參見“反射的實現(xiàn)原理”)。示例如下:
#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()是必須的孕惜。通過這兩個宏,我們給類和結構體注入了實現(xiàn)反射所必須的額外函數(shù)和類型信息晨炕。
在代碼中衫画,第一個反射屬性是ResourcesToGather。它被指定為EditAnywhere 和Category=Pawn瓮栗。這意味著這個屬性可以在任意屬性面板里編輯削罩,并且屬于Pawn這個類別。此外好幾個函數(shù)指定了BlueprintCallable和一個類別费奸,這意味這些函數(shù)都可以在Blueprints里被調用弥激。
如MyTeamNum聲明所示,在同一個類中混雜反射成員和非反射成員是沒有問題的愿阐,只是要注意的是非反射成員對于所有基于反射的系統(tǒng)來說都是不可見的(比如緩存一個非反射的UObject指針通常來說是危險的微服,因為垃圾回收器并不知道你引用了它)。
你可以在ObjectBase.h里找到每一個說明符關鍵字(比如EditAnywhere和BlueprintCallable)的一段簡短注釋和用法說明缨历。如果你不知道某個關鍵字是起做什么用的以蕴,按快捷鍵Alt+G會跳轉到ObjectBase.h中對應的定義上去了(這些不是真的C++關鍵字,但是智能提示和VAX看起來不關心也分不清它們間的區(qū)別)辛孵。
更多的信息可以參見官網(wǎng)上的Gameplay Programming Reference丛肮。
局限
UHT并不是一個真正的C++解析器。它可以識別語言的常見子集魄缚,并且在解析時盡可能多的跳過任何它認為不相關的代碼宝与;同時僅關注反射的類,函數(shù)和屬性。盡管這樣伴鳖,某些情況下它還是會出錯的节值,所以當往一個已有的頭文件里加上反射類型時,你可能要重寫一些代碼榜聂,或者要把已有的代碼包在#if CPP / #endif里搞疗。你應該盡量避免把反射宏修飾過的屬性或者函數(shù)包在 #if/#ifdef (WITH_EDITOR 和WITH_EDITORONLY_DATA除外)里,這是因為在某些構建配置里這些宏定義有可能不是true的须肆,那么在生成的代碼里去引用這些屬性或者函數(shù)時就會報編譯錯誤了匿乃。
對于虛幻引擎的反射來說,C++中絕大多數(shù)的數(shù)據(jù)類型都是支持的豌汇,但是并不是所有(特別是只有少數(shù)模版類型是支持的幢炸,比如TArray和TSubclassOf,并且它們的模版參數(shù)不能是嵌套類型)拒贱。如果你使用反射宏來修飾無法在運行時表示的數(shù)據(jù)類型宛徊,UHT會給你報一個描述性質的錯誤信息。
使用反射信息
雖然大部分的游戲代碼在運行時使用反射系統(tǒng)給予的便利的同時逻澳,可以忽略反射系統(tǒng)闸天,但是當你編寫工具或者玩法系統(tǒng)時會發(fā)現(xiàn)反射還是很有用的。
反射系統(tǒng)的類型層級如下所示:
UField
UStruct
UClass (C++ class)
UScriptStruct (C++ struct)
UFunction (C++ function)
UEnum (C++ enumeration)
UProperty (C++ member variable or function parameter)
(更多不同類型的子類)
UStruct是聚合類結構(任何包含了其他成員的類型斜做,比如C++類苞氮,結構,或者函數(shù))的基礎類型瓤逼,不要把它和C++的結構混淆起來(與它對應的是UScriptStruct)笼吟。UClass可以包含函數(shù)或者屬性作為它的成員,但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的模版參數(shù)用作過濾器(通過該參數(shù)你可以使用UField來同時查看屬性和函數(shù)诲侮,或者其中任意一個)。迭代器構造函數(shù)的第二個參數(shù)用來指定是否只需訪問該類或者結構體中的屬性/函數(shù)箱蟆,還是也同時訪問基類/結構(默認)沟绪;這個參數(shù)取值不會對函數(shù)有任何的影響。
每個類型都有一組唯一的標志位(EClassFlags + HasAnyClassFlags, etc…)和一個繼承自UField的通用元數(shù)據(jù)存儲系統(tǒng)空猜。反射宏中的說明符關鍵字可以作為標志位存儲也可以作為元數(shù)據(jù)存儲绽慈,取決于它們是被用于游戲運行時恨旱,還是只是在編輯器里。這樣就可以實現(xiàn)在游戲運行時去除僅在編輯器用的元數(shù)據(jù)來達到節(jié)省內存的目的坝疼,而作為標志位存儲的則一直可用搜贤。
你可以通過應用反射數(shù)據(jù)來實現(xiàn)不同的功能(比如枚舉屬性,按數(shù)據(jù)驅動的方式獲取/設置屬性钝凶,調用反射方法仪芒,甚至創(chuàng)建新實例);與其深入了解每個反射特性耕陷,不如瀏覽一下UnrealType.h 和 Class.h掂名,然后跟蹤調試一個和你要做的功能相近的樣例來得更容易一些。
反射的實現(xiàn)原理
如果你僅僅只是想使用反射系統(tǒng)而已哟沫,那么可以毫不猶豫的跳過這一部分饺蔑;但是知道它是怎么工作的可以讓你在使用時更好的做出決定并了解它的局限性。
Unreal Build Tool (UBT) 和 Unreal Header Tool (UHT)在實現(xiàn)運行時的反射功能中扮演著核心的角色嗜诀。UBT的工作就是掃描頭文件猾警,如果一個頭文件包含至少一個反射類型則記錄該頭文件所在的模塊。如果這些頭文件在編譯之后發(fā)生改變隆敢,UHT就會被喚起收集并更新對應的反射數(shù)據(jù)发皿。UHT解析頭文件,創(chuàng)建反射數(shù)據(jù)集合筑公,然后生成包含反射數(shù)據(jù)(包含在每個模塊都有的.generated.inl里)以及各類輔助類和函數(shù)(包含在每個頭文件對應的.generated.h里)的C++代碼。
之所以通過生成的C++代碼來保存反射信息尊浪,一個主要的好處是這樣可以確保這些信息和最終的二進制文件保持同步匣屡。把反射信息和引擎代碼一起編譯,并在啟動時通過C++表達式來計算成員的偏移等信息拇涤,而不是逆向工程某個特定的平臺/編譯器/優(yōu)化選項的組合捣作,這樣你永遠都不會加載到錯誤的反射信息。UHT作為一個獨立的程序鹅士,它不會修改任何生成的頭文件券躁,這樣就避免了在UE3腳本編譯中經常被抱怨的先有蛋還是先有雞這樣的問題。
生成的方法包括像StaticClass() / StaticStruct()掉盅,方便你獲得某個類型反射數(shù)據(jù)的類型也拜,生成的代碼段則方便你在Blueprints或者網(wǎng)絡傳輸中調用。這些東西必須作為類或者結構體的一部分來聲明趾痘,這就是為什么GENERATED_UCLASS_BODY() 或GENERATED_USTRUCT_BODY()宏要包含在反射類型里慢哈,以及頭文件中需加入包含定義這些宏的代碼#include “TypeName.generated.h” 的原因。