UE4——反射系统
反射是程序在运行时进行自检的一种能力。它非常有用且在虚幻引擎中基础技术,支撑了诸如 编辑器中的细节面板、序列化、垃圾回收、网络复制、以及蓝图与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在运行时会给出一个比较详细的错误描述。
使用反射数据
大多数的游戏代码可以在运行时忽略属性系统,也可以享受它给你带来的好处,但是当你在写工具代码或者构建游戏性系统的时候,你就会觉得它很有用了。
属性系统的类型层次大约如下所示:
UFied
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++中的结构体(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"的头文件中定义了这些宏。