虚幻引擎中的反射(译)

原文链接:https://www.unrealengine.com/en-US/blog/unreal-property-system-reflection?sessionInvalidated=true

反射是程序的一种能力,借助于它可以在运行时查看自身。作为虚幻引擎中的基础技术,它相当有用,增强了众多的系统比如编辑器中的属性面板,对象序列化,垃圾回收,网络对象传输以及蓝图脚本和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” 的原因。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容

  • 在瑜伽中,以莲花姿势静坐,气定凝神,心中摒除杂念,静若清池,不是件容易的事,而更不容易的是以头倒立的姿势静心。 很...
    袁一今阅读 1,336评论 6 12
  • 在自由操作时间里龙龙取的工作是“植物生长过程”龙龙在操作这份工作时表现的很熟练哦,在和卡片配对的时候龙龙能准确的把...
    a81c671c0ae2阅读 394评论 0 0
  • 1、混淆平台型商业模式和组织模式。互联网企业中绝大部分依然是由“初创期的创业热情”和“互联网时代造就的红利”支撑,...
    花得一阅读 294评论 0 0
  • 爱徒 王正华(四.5) 妈妈去年买了一盆兰花,刚买来时,我还以为那是一棵韭菜:它叶片细细的,又浓又绿,还有些发黑,...
    从源王有鹏阅读 241评论 0 1