原文地址:Unreal Property System (Reflection)
Reflection is the ability of a program to examine itself at runtime. This is hugely useful and is a foundational technology of the Unreal engine, powering many systems such as detail panels in the editor, serialization, garbage collection, network replication, and Blueprint/C++ communication. However, C++ doesn’t natively support any form of reflection, so Unreal has its own system to harvest, query, and manipulate information about C++ classes, structs, functions, member variables, and enumerations. We typically refer to reflection as the property system since reflection is also a graphics term.
The reflection system is opt-in. You need to annotate any types or properties that you want to be visible to the reflection system, and Unreal Header Tool (UHT) will harvest that information when you compile your project.
反射机制是程序在运行时可以审查自身细节的一种能力。这项能力非常有用并且是虚幻引擎一种基础的技术,虚幻引擎在此基础上提供一系列的系统,比如编辑器中的细节面板、序列化、垃圾收集、网络应答以及蓝图与C++的通信等。然而,C++本身并不支持任何的反射形式,所以虚幻引擎使用自身的一套系统来收集、查询和管理C++相关的类、结构体、函数、成员变量以及枚举器等信息。我们这里所说的反射通常指的是属性系统中的反射机制,而不是指光学图形术语中的反射现象。
反射系统是可以选择性加入的,你需要事先标记那些你想要被反射系统访问的任何类型和属性,然后Unreal Header Tool (UHT)
工具将会在你编译项目的时候收集这些信息。
Markup
To mark a header as containing reflected types, add a special include at the top of the file. This lets UHT know that they should consider this file, and it’s also required for the implementation of the system (see the ‘A peek behind the curtain’ section for more information).
#include "FileName.generated.h"
You can now use UENUM(), UCLASS(), USTRUCT(), UFUNCTION(), and UPROPERTY() to annotate different types and member variables in the header. Each of these macros goes before the type or member declaration, and can contain additional specifier keywords. Let’s take a look at a real world example (from StrategyGame):
要标记一个包含反射类型的头文件,需要在文件的顶部添加一个特殊的包含信息(如下代码),这样可以让UHT
工具知道它应该考虑这个文件,并且在系统实现方面的还有一定的要求(你可以参考“表象之后的秘密”章节来获得更多信息,本文最后一节)
你可以使用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]
};
This header declares a new class called AStrategyChar deriving from ACharacter. It uses UCLASS() to indicate it is reflected, which is also paired with a macro GENERATED_UCLASS_BODY() inside of the C++ definition. The GENERATED_UCLASS_BODY() / GENERATED_USTRUCT_BODY() macros are required in reflected classes or structs, as they inject additional functions and typedefs into the class body.
The first property shown is ResourcesToGather, which is annotated with EditAnywhere and Category=Pawn. This means the property can be edited in any details panel in the editor, and will show up in the Pawn category. There are a couple of annotated functions marked with BlueprintCallable and a category, meaning they’ll be available to call from Blueprints.
As the MyTeamNum declaration shows, it’s fine to mix reflected and non-reflected properties in the same class, just be aware that the non-reflected properties are invisible to all of the systems that rely on reflection (e.g., storing a raw unreflected UObject pointer is usually dangerous since the garbage collector can’t see your reference).
Each of the specifier keywords (such as EditAnywhere or BlueprintCallable) is mirrored in ObjectBase.h with a short comment on the meaning or usage. If you’re not sure what a keyword does, Alt+G will usually work to take you to the definition in ObjectBase.h (they aren’t real C++ keywords, but Intellisense or VAX don’t seem to mind/understand the difference).
Check out the Gameplay Programming Reference for more information.
这个头文件声明了一个继承自ACharacter
名为AStrategyChar
的类,它使用了UCLASS()
来表明它参与反射机制,而且这个宏还有一个与之配对的名为GENERATED_UCLASS_BODY()
的宏安插在C++的类的定义之中。GENERATED_UCLASS_BODY() / GENERATED_USTRUCT_BODY()
这两个宏要求被添加到对应的类或者结构体中,因为他们会在类的定义体中添加额外的函数或者类型别名。
类的第一个成员变量名为ResourcesToGather
,该成员变量被注明的反射属性有EditAnywhere
和Category=Pawn
,这表明当前变量的值能够在编辑器的任意一个细节面板中编辑,并且显示在’Pawn’分组中。另外还有一对被标记为BlueprintCallable
和不同种类的函数,这表明他们能够被蓝图函数调用。
成员变量MyTeamNum
的定义展示出,在一个类中同时定义参与反射机制的变量和非反射变量能够工作的很好,你仅仅需要知道不参与反射的属性变量对于依赖反射机制的系统都是不可见的(例如:存储一个不参与反射的UObject
类型的指针通常是非常危险的,因为垃圾回收机制不能收集到它的引用)。
每一个指定关键字(比如EditAnywhere
或BlueprintCallable
)都定义在文件’ObjectBase.h’,并且带有一段简短的关于含义和使用方法的评论信息,如果你不确定使用什么具体的关键字,(选中一个关键字使用)快捷键Alt+G
通常会跳转到该关键字在文件’ObjectBase.h’中定义的地方(它们并不是真正的C++的关键字,但是智能感知或者’VAX’插件并不能辨别这些不同)。
查看Gameplay Programming Reference章节获得更多信息.
Limitations
UHT isn’t a real C++ parser. It understands a decent subset of the language and actively tries to skip any text that it can; only paying attention to reflected types, functions, and properties. However, some things can still confuse it, so you may have to reword something or wrap it in an #if CPP / #endif pair when adding a reflected type to an existing header. You should also avoid using #if/#ifdef (except for WITH_EDITOR and WITH_EDITORONLY_DATA) around any annotated properties or functions, since the generated code references them and will cause compile errors in any configuration where the define isn’t true.
Most common types work as expected, but the property system can’t represent all possible C++ types (notably only a few template types such as TArray and TSubclassOf are supported, and their template parameters cannot be nested types). UHT will give you a descriptive error message if you annotate a type that can’t be represented at runtime.
UHT
工具事实上不是一个真正的C++解析器,它能理解C++语言一个合理的子集并积极的略过它所能忽略的文本,只关注那些参与反射机制的类型、函数和属性。然而,一些做法仍然会使整个工具感到迷惑,所以当你添加一个反射类型到已经存在的头文件中时,也许需要改写一些代码或者把它包装到#if CPP / #endif
宏定义之间。你应该避免使用#if/#ifdef
(除了WITH_EDITOR
和WITH_EDITORONLY_DATA
)来围绕被标记的属性和函数,因为产生的代码会引用它们,并且在任何一个定义不正确的配置的地方产生编译错误。
大多数的通用类型能如预期般地工作,但是属性系统不能表示出所有的可能的C++类型(尤其是一些模板类型,仅仅有TArray
和TSubclassOf
被支持,并且他们的模板参数不能够嵌套)。如果你标注了一个它不能在运行时正确表现的类型,UHT
工具将会给出一个描述性的错误信息。
Using reflection data
Most game code can ignore the property system at runtime, enjoying the benefits of the systems that it powers, but you might find it useful when writing tool code or building gameplay systems.
The type hierarchy for the property system looks like this:
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 is the basic type of aggregate structures (anything that contains other members, such as a C++ class, struct, or function), and shouldn’t be confused with a C++ struct (that’s UScriptStruct). UClass can contain functions or properties as their children, while UFunction and UScriptStruct are limited to just properties.
You can get the UClass or UScriptStruct for a reflected C++ type by writing UTypeName::StaticClass() or FTypeName::StaticStruct(), and you can get the type for a UObject instance using Instance->GetClass() (it’s not possible to get the type of a struct instance since there is no common base class or required storage for structs).
for (TFieldIterator
PropIt(GetClass()); PropIt; ++PropIt) { UProperty* Property = *PropIt; // Do something with the property } The template argument to TFieldIterator is used as a filter (so you can look at both properties and functions using UField, or just one or the other). The second argument to the iterator constructor indicates whether you only want fields introduced in the specified class/struct, or fields in the parent class/struct as well (the default); it doesn’t have any effect for functions.
Each type has a unique set of flags (EClassFlags + HasAnyClassFlags, etc…), as well as a generic metadata storage system inherited from UField. The keyword specifiers are usually either stored as flags or metadata, depending on whether they are needed in a runtime game, or only for editor functionality. This allows the editor-only metadata to be stripped out to save memory, while the runtime flags are always available.
You can do a lot of different things using the reflection data (enumerating properties, getting or setting values in a data-driven manner, invoking reflected functions, or even constructing new objects); rather than go in depth on any one case here, it’s probably easier to have a look thru UnrealType.h and Class.h, and track down an example of code that does something similar to what you want to accomplish.
大多数的游戏代码在运行时能够忽略属性系统的存在,享受这个系统强大力量所带来的好处,但是当你编写工具代码或者编译游戏系统的时候会发现了解它还是很有用的。
属性系统的继承关系看起来像这样:
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++结构体、C++函数),你不要把它和一个C++结构体(那个是UScriptStruct
)弄混淆了。UClass
能够包含函数和属性来作为它的子结构,而UFunction
和UScriptStruct
被限定只能用来作为属性。
你能通过写代码UTypeName::StaticClass()
或FTypeName::StaticStruct()
来为一个C++反射类型获取UClass
或者USctriptStruct
,你也能通过使用Instance->GetClass()
来为UObject
实例获得类型(你不可能获得结构体实例的类型,因为它没有一个通用的基类或者必要的存储)
想要迭代Ustruct
所有的成员,你可以使用TFieldIterator
如下操作:
for (TFieldIterator PropIt(GetClass()); PropIt; ++PropIt)
{
UProperty* Property = *PropIt;
// Do something with the property
}
TFieldIterator
的模板参数被使用作为一个过滤器(所以你能够看到属性和函数会使用UField
,或者只有一个,或者是其他的),第二个参数是用于迭代器的构造函数,它用来表明你是否只想来了解特定的类或者结构体,或者只是他们的属性(默认选项),这个参数对于函数来说没有任何影响。
每一种类型都有一组独特的标志(比如EClassFlags + HasAnyClassFlags
等等),并且有一个通用的继承自UField
的元数据存储系统。关键字指定它们存储为一个标志或者元数据,依据是它是否需要在游戏运行时所必须,或者说它仅仅是编辑器所需要,这就允许在游戏运行的的标志起作用时,那些仅仅是编辑器所需的元数据可以被跳过用来节省内存。
你能使用反射数据做许多不同的事情(遍历所有的属性,以数据驱动的方式来获取或者设置属性的值,调用反射的函数,甚至是构造新的对象),而不是仅仅在一个示例上深入研究,你可以很容易的通过查看头文件UnrealType.h
和Class.h
来找到一个与自己当前任务非常相似的一个代码示例。
A peek behind the curtain
You can safely skip this section if you just want to use the property system, but knowing how it works helps motivate some of the decisions and limitations in headers that contain reflected types.
Unreal Build Tool (UBT) and Unreal Header Tool (UHT) act in concert to generate the data that is needed to power runtime reflection. UBT has to scan headers to do its job, and it remembers any modules that contain a header with at least one reflected type. If any of those headers have changed since the last compile, UHT is invoked to harvest and update the reflection data. UHT parses the headers, builds up a set of reflection data, and then generates C++ code containing the reflection data (contributing to a per-module .generated.inl), as well as various helpers and thunk functions (per-header .generated.h).
One of the major benefits of storing the reflection data as generated C++ code is that it is guaranteed to be in sync with the binary. You can never load stale or out of date reflection data since it’s compiled in with the rest of the engine code, and it computes member offsets/etc… at startup using C++ expressions, rather than trying to reverse engineer the packing behavior of a particular platform/compiler/optimization combo. UHT is also built as a standalone program that doesn’t consume any generated headers, so it avoids the chicken-and-egg issues that were a common complaint with the script compiler in UE3.
The generated functions include things like StaticClass() / StaticStruct(), which make it easy to get reflection data for the type, as well as thunks used to call C++ functions from Blueprints or network replication. These must be declared as part of the class or struct, which explains why a GENERATED_UCLASS_BODY() or GENERATED_USTRUCT_BODY() macro is included in your reflected types, as well as the #include “TypeName.generated.h” which defines those macros.
如果你只是想使用属性系统,你可以毫无顾忌的跳过这一节,但是知道它是怎样工作的将会帮助你激发一些关于包含反射类型的决策和限制。
Unreal Build Tool (UBT)
工具和 Unreal Header Tool (UHT)
工具一致合作来产生运行时反射所需的数据,UBT
工具必须去浏览头文件来作为它的工作,它需要记录那些至少包含一个反射类型属性的头文件的模块,如果这些头文件在编译结束时发现有改变,则UHT
工具将会被调用来收集和更新反射数据,UHT
工具解析那些头文件,然后建立一个反射数据的集合,并且产生包含反射数据(产生到per-module.generated.inl文件中)的C++代码、种类繁多的帮助和程序块(在per-header.generated.h文件中)。
把反射数据作为产生的C++代码来存储的主要益处是保证与二进制数据同步,你将永远不会加载残缺的或者过期的反射数据,因为它与引擎的代码编译到了一起,并且它会计算成员的偏移量等等。在启动时使用C++表达式,而不是尝试去逆向设计一个平台、编译器和优化器组合体的打包行为。UHT
工具也是一个单独的程序,并且互惠影响原有产生的头文件,因此它避免了在UE3脚本引擎中那个“先有鸡还是先有蛋”的普遍的冲突问题。
被产生出来的函数包括了StaticClass() / StaticStruct()
,它们使你通过类型获得反射数据、通过蓝图或者网络回应来调用C++函数变得非常容易,它们必须定义为类和结构体的一部分,这就解释了为什么要在你的反射类型中添加GENERATED_UCLASS_BODY()
或者 GENERATED_USTRUCT_BODY()
这两个宏,也解释了为什么要在定义这些宏的文件中添加#include "TypeName.generated.h"
。