原文作者:Michael Noland
链接:https://www.unrealengine.com/zh-CN/blog/unreal-property-system-reflection
反射是程序在运行时自检的一种能力。这是非常有用的,并且在虚幻引擎中这也是非常基础的一项技术,在它的加持下,虚幻引擎具备了编辑器内的细节面板、序列化、垃圾回收、网络复制以及蓝图和C++之间的通信。但是,C++本身并不支持任何形式的反射,因此虚幻构建起了自己的系统来搜集、查询以及操作C++类、结构体、函数、成员变量以及枚举的信息。通常我们提到反射即是在说这套属性系统,因为反射同时也是图形学的一个术语。
反射系统是可选的。你需要去标记任何你想要在属性系统中可见的类型或者是属性,然后虚幻头文件工具(UHT)才会在你编译你的项目时收集这些信息。
标记
为了将一个头文件标记为包含反射类型,可以在文件的顶部添加一个特别的include。这会告知UHT,让他们将这个文件考虑在内,这也是系统实现需要求的操作。
#include “FileName.generated.h”
在进行了上述包含之后,你就可以在头文件里使用UENUM(),UCLASS(),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]
};
这个头文件申明了一个新类 AStrategyChar,继承自ACharacter。它使用UCLASS()来表明自身加入了反射系统,它是与宏GENERATED_UCLASS_BODY()共同发挥作用。对于反射的类或者结构体来说,GENERATED_UCLASS_BODY()或者GENERATED_USTRUCT_BODY是必须的,因为他们会向类或者结构体内部添加额外的函数和typedef。
上面那个类中的第一个属性就是ResourcesToGather,这里使用了EditAnywhere和Category=Pawn来进行标记。这意味着该属性可以在编辑器的任意细节面板进行编辑,并且将会显示到“Pawn”这个分类之下。还有一些用BlueprintCallable以及分类标记的函数,这也意味着他们可以通过蓝图进行调用。
同时,通过MyTeamNum的申明也可以看得出来,将反射和非反射的属性混在同一个类中也是ok的,只是需要时刻记得:非反射的属性对于依赖反射的系统来说是不可见的(即,存储非反射的原始UObject指针通常是很危险的,因为垃圾收集器看不到您的引用)。每个说明符关键字(如EditAnywhere或BlueprintCallable)都反映在ObjectBase.h(纠正:应该是位于ObjectMacros.h)中,并对其含义或用法进行了简短注释。如果你不确定某个关键字的用法,可以到该头文件中去查阅。
可以参考Gameplay Programming Reference(https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/GameplayArchitecture/)来查阅更多的说明性信息。
限制
UHT并不是真正C++解析器。它理解语言的其中一个的子集,并且会去尝试跳过它可以跳过的任何文本;即它只会去关注那些支持反射类型、函数和属性。但是,仍然存在一些混淆的情况发生,因此你也许最好在头文件中重写一些内容或者将它包装在 #if CPP/#endif 组合中。您还应该避免在任何带注释的属性或函数周围使用 #if/#ifdef(除了WITH_EDITOR和WITH_EDITORONLY_DATA),因为生成的代码引用了这些属性或函数,并且在定义不正确的任何配置中都会导致编译错误。
大多数常见的类型都可以像预期那样正常工作,但是属性系统不能表示所有可能的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)
(Many subclasses for different types)
UStruct是数据集合结构(任何包括其他类型的结构,例如C++类,结构体或者函数)的基本类型,并且需要将它和C++中的结构体(对应UScriptStruct)区分开。UClass可以包含函数或者属性,而UFunction(译者注:这里应该是想说UProperty)和UScriptStruct仅限于属性。
通过编写 UTypeName::StaticClass() 或 FTypeName::StaticStruct(),你可以得到支持反射C++类型的 UClass 或者 UScriptStruct,并且可以使用 instance->GetClass() 获得 UObject 实例的类型(因为没有公共的基类,或结构的必需内存,所以不可能得到结构实例的类型)。
要迭代一个UStruct的所有成员,使用TFieldIterator:
for (TFieldIterator<UProperty> PropIt(GetClass()); PropIt; ++PropIt)
{
UProperty* Property = *PropIt;
// Do something with the property
}
TFieldIterator的模板参数用作过滤器(因此你可以使用UField查看属性和函数,或者只查看两者中的一个)。迭代器构造器的第二个参数指出你是只希望在指定的类/结构中引入字段,还是也希望在父类/结构中引入字段(默认值);它对函数没有任何影响。
每种类型都有一组唯一的标志(EClassFlags+HasAnyClassFlags等),以及从UField继承的通用元数据存储系统。关键字说明符通常存储为标志或元数据,具体取决于运行时游戏中是否需要它们,或者仅用于编辑器功能。这允许剥离掉仅用于编辑器的元数据以节省内存,而运行时标志则是始终可用的。
你可以使用反射数据做非常多不同的事(枚举属性,以数据驱动的方式获取或者设置值,调用反射函数,或者甚至构造新的对象);与其深入讨论这里的任何一种情况,不如通过UnrealType.h和Class.h进行查看,并找到一个与您想要完成的类似的代码示例。
稍微深入一点
如果你只是想要使用属性系统的话,你可以大胆得跳过这一小节。了解属性系统背后的机理可以让你在包含反射类型的头文件里做的每个决定以及遇到的限制都清楚背后潜在的原因。
UBT和UHT协同来生成支持运行时反射的数据。UBT需要去扫所有的头文件来完成它的工作,并且它会记住所有的包含至少一个反射数据的头文件所在的模块。UHT会对所有的头文件进行解析,建立起一系列的反射数据,然后生成包含反射数据的C++代码(到per-module.generated.inl),以及多种帮助件和辅助函数(到per-header.generated.h)。
用生成C++代码的方式来存储反射数据的其中一项好处是它保障了与二进制文件的同步。你不能加载过时的反射数据,因为它是用其他引擎代码编译的,它计算成员偏移量等。在启动时使用C++表达式,而不是试图反向设计特定平台/编译器/优化组合的打包行为。UHT也被构建为一个独立的程序,它不消耗任何生成的头文件,从而避免了是先有鸡还是现有蛋的问题。
生成的函数包括像StaticClass()/StaticStruct,这使得很容易可以获得反射数据的类型信息,以及用来从蓝图或者网络复制调用C++函数的thunk函数。这些必须被申明为类或者结构体的一部分,这也解释了为什么在反射类型中需要包含GENERATED_UCLASS_BODY()或者GENERATED_USTRUCT_BODY()宏,以及详细定义了这些宏的 TypeName.generated.h。