《InsideUE4》UObject(三)类型系统设定和结构

垃圾分类,从我作起!html

引言

上篇咱们谈到了为什么设计一个Object系统要从类型系统开始作起,并探讨了C#的实现,以及C++中各类方案的对比,最后获得的结论是UE采用UHT的方式搜集并生成反射所需代码。接下来咱们就应该开始着手设计真正的类型系统结构。
在以后的叙述中,我会同时用两个视角来考察UE的这套Object系统:
一是以一个通用的游戏引擎开发者角度来从零开始设计,设想咱们正在本身实现一套游戏引擎(或者别的须要Object系统的框架),在体悟UE的Object系统的同时,思考哪些是真正的核心部分,哪些是后续的锦上添花。踏出一条重建Object系统的路来。
二是以当前UE4的现状来考量。UE的Object系统从UE3时代就已经存在了(再远的UE3有知道的前辈还望告知),历经风雨,修修补补,又通过UE4的大改造,因此一些代码读起来非常诘屈聱牙,笔者也并不敢说通晓每一行代码写成那样的起因,只能尽可能从UE的角度去思考这么写有什么用意和做用。同时咱们也要记得UE是很博大精深没错,但并不表明每一行代码都完美。总体结构上很优雅完善,但也一样有不少小漏洞和缺陷,也并非全部的实现都是最优的。因此也支持读者们在了解的基础上进行源码改造,符合本身自己的开发需求。c++

PS:类型系统不可避免的谈到UHT(Unreal Header Tool,一个分析源码标记并生成代码的工具),但本专题不会详细叙述UHT的具体工做流程和原理,只假定它万事如我心意,UHT的具体分析后续会有特定章节讨论。express

设定

先假定咱们已经接受了UE的设定:
在c++写的class(struct同样,只是默认public而已)的头上加宏标记,在其成员变量和成员函数也一样加上宏标记,大概就是相似C#Attribute的语法。在宏的参数能够按照咱们自定的语法写上内容。在UE里咱们就能够看到这些宏标记:数组

#define UPROPERTY(...)
#define UFUNCTION(...)
#define USTRUCT(...)
#define UMETA(...)
#define UPARAM(...)
#define UENUM(...)
#define UDELEGATE(...)
#define UCLASS(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_PROLOG)
#define UINTERFACE(...) UCLASS()

真正编译的时候,大致上都是一些空宏。UCLASS有些特殊,通常状况下最后也都是空宏,另一些状况下会生成一些特定的事件参数声明等等。不过这暂时跟本文的重点无关。这里重点有两点,一是咱们能够经过给类、枚举、属性、函数加上特定的宏来标记更多的元数据;二是在有必要的时候这些标记宏甚至也能够安插进生成的代码来合成编译。
咱们也暂时不用管UHT到底应该怎么实现,就也先假定有那么一个工具会在每次编译前扫描咱们的代码,获知那些标记宏的位置和内容,并紧接着分析下一行代码的声明含义,最后生成咱们所须要的代码。
还有两个小问题是:微信

为什么是生成代码而不是数据文件?
毕竟C++平台和C#平台不同,同时在引用1里的UnrealPropertySystem(Reflection)里也提到了最重要的区分之处:网络

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.架构

简单来讲就是避免了不一致性,不然又得有机制去保证数据文件和代码能匹配上。同时跨平台需求也很难保证结构间的偏移在各个平台编译器优化的不一样致使得差别。因此还不如简单生成代码文件一块儿编译进去得了。框架

若是标记应该分析哪一个文件?
既然是C++了,那么生成的代码天然也差很少是.h.cpp的组合。假设咱们为类A生成了A.generated.h和A.generated.cpp(按照UE习俗,名字无所谓)。此时A.h通常也都须要Include "A.generated.h",好比类A的宏标记生成的代码若是想跟A.generated.h里咱们生成的代码来个内外夹攻的话。另外一方面,用户对背后的代码生成应该是保持最小惊讶的,用户写下了A.h,他在使用的时候天然也会想include "A.h",因此这个时候咱们的A.generated.h就得找个方式一块儿安插进来,最方便的方式莫过于直接让A.h include A.generated.h了。那既然每一个须要分析的文件最后都会include这么一个*.generated.h,那天然就能够把它自己就看成一种标记了。因此UE目前的方案是每一个要分析的文件加上该Include而且规定只能看成最后一个include,由于他也担忧会有各类宏定义顺序产生的问题。编辑器

#include "FileName.generated.h"

若是你一开始想的是给每一个文件也标记个空宏,其实倒也无不可,只不过没有UE这么简洁。可是好比说你想控制你的代码分析工具在分析某个特定文件的时候专门定制化一些逻辑,那这种像是C#里AssemblyAttribute的文件宏标记就显示出做用了。UHT目前不须要因此没作罢了。ide

结构

在接受了设定以后,是否是以为原本这个写法有点怪的Hello类看起来也有点可爱呢?

#include "Hello.generated.h"
UClass()
class Hello
{
public:
    UPROPERTY()
    int Count;
    UFUNCTION()
    void Say();
};

先什么都无论,伪装UHT已经为咱们搜集了完善的信息,而后这些信息在代码里应该怎么储存?这就要谈到一些基本的程序结构了。一个程序,简单来讲,能够认为是由众多的类型和函数嵌套组成的,类型有基础类型,枚举,类;类里面可以再定义字段和函数,甚至是子类型;函数有输入和输出,其内部也依然能够定义子类型。这是C++的规则,但你在支持的时候就能够在上面进行缩减,好比你就能够不支持函数内定义的类型。
先来看看UE里造成的结构:
UFieldAndChildren2.jpg-35.9kB
C++有声明和定义之分,图中黄色的的均可以看做是声明,而绿色的UProperty能够看做是字段的定义。在声明里,咱们也能够把类型分为可聚合其余成员的类型和“原子”类型。

  • 聚合类型(UStruct):
    • UFunction,只可包含属性做为函数的输入输出参数
    • UScriptStruct,只可包含属性,能够理解为C++中的POD struct,在UE里,你能够看做是一种“轻量”UObject,拥有和UObject同样的反射支持,序列化,复制等。可是和普通UObject不一样的是,其不受GC控制,你须要本身控制内存分配和释放。
    • UClass,可包含属性和函数,是咱们日常接触到最多的类型
  • 原子类型:
    • UEnum,支持普通的枚举和enum class。
    • int,FString等基础类型不必特别声明,由于能够简单的枚举出来,能够经过不一样的UProperty子类来支持。

把聚合类型们统一块儿来,就造成了UStruct基类,能够把一些通用的添加属性等方法放在里面,同时能够实现继承。UStruct这个名字确实比较容易引发歧义,由于实际上C++中USTRUCT宏生成了类型数据是用UScriptStruct来表示的。
还有个类型比较特殊,那就是接口,能够继承多个接口。跟C++中的虚类同样,不一样的是UE中的接口只能够包含函数。通常来讲,咱们本身定义的普通类要继承于UObject,特殊一点,若是是想把这个类看成一个接口,则须要继承于UInterface。可是记得,生成的类型数据依然用UClass存储。从“#define UINTERFACE(...) UCLASS()”就能够看出来,Interface其实就是一个特殊点的类。UClass里经过保存一个TArray<FImplementedInterface> Interfaces数组,其子项又包含UClass* Class来支持查询当前类实现了那些接口。

最后是定义,在UE里是UProperty,能够理解为用一个类型定义个字段“type instance;”。UE有Property,其Property有子类,子类之多,一屏列不下。实际深刻代码的话,会发现UProperty经过模板实例化出特别多的子类,简单的如UBoolProperty、UStrProperty,复杂的如UMapProperty、UDelegateProperty、UObjectProperty。后续再一一展开。

元数据UMetaData其实就是个TMap<FName, FString>的键值对,用于为编辑器提供分类、友好名字、提示等信息,最终发布的时候不会包含此信息。

为了加深一下概念,我列举一些UE里的用法,把图和代码加解释一块儿关联起来理解的会更深入些:

#include "Hello.generated.h"
UENUM()
namespace ESearchCase
{
    enum Type
    {
        CaseSensitive,
        IgnoreCase,
    };
}

UENUM(BlueprintType)
enum class EMyEnum : uint8
{
    MY_Dance    UMETA(DisplayName = "Dance"),
    MY_Rain     UMETA(DisplayName = "Rain"),
    MY_Song     UMETA(DisplayName = "Song")
};

USTRUCT()
struct HELLO_API FMyStruct
{
    GENERATED_USTRUCT_BODY()
    
    UPROPERTY(BlueprintReadWrite)
    float Score;
};

UCLASS()
class HELLO_API UMyClass : public UObject
{
    GENERATED_BODY()
public:
    UPROPERTY(BlueprintReadWrite, Category = "Hello")
    float Score;

    UFUNCTION(BlueprintCallable, Category = "Hello")
    void CallableFuncTest();
    
    UFUNCTION(BlueprintCallable, Category = "Hello")
    void OutCallableFuncTest(float& outParam);

    UFUNCTION(BlueprintCallable, Category = "Hello")
    void RefCallableFuncTest(UPARAM(ref) float& refParam);

    UFUNCTION(BlueprintNativeEvent, Category = "Hello")
    void NativeFuncTest();

    UFUNCTION(BlueprintImplementableEvent, Category = "Hello")
    void ImplementableFuncTest();
};

UINTERFACE()
class UMyInterface : public UInterface
{
    GENERATED_UINTERFACE_BODY()
};

class IMyInterface
{
    GENERATED_IINTERFACE_BODY()

    UFUNCTION(BlueprintImplementableEvent)
    void BPFunc() const;

    virtual void SelfFunc() const {}
};

先不用去管宏里面参数的含义,目前先造成大局的印象。可是注意,我这里没有提到蓝图里能够建立的枚举、接口、结构、类等。它们也都是相应的从各自UEnum、UScriptStruct、UClass再派生出来。这个留待以后再讲。读者们须要明白的是,一旦咱们可以用数据来表达类型了,咱们就能够自定义出不一样的数据来动态建立出不一样的其余类型。

思考:为何还须要基类UField?
UStruct好理解,表示聚合类型。那为何不直接UProperty、UStruct、UEnum继承于UObject?在笔者看来,主要有三点:

  1. 为了统一全部的类型数据,若是全部的类型数据类都有个基类的话,那么咱们就很容易用一个数组把全部的类型数据都引用起来,能够方便的遍历。另外也关乎到一个顺序的问题,好比在类型A里定义了P一、F一、P二、F2,属性和函数交叉着定义,在生成类型A的类型数据UClass内部就也能够是以一样的顺序,之后要是想回溯出来一份定义,也能够跟原始的代码顺序一致,若是是用属性和函数分开保存的话,就会麻烦一些。
  2. 如上图可见,全部的无论是声明仍是定义(UProperty、UStruct、UEnum),均可以附加一份额外元数据UMetaData,因此应该在它们的基类里保存。
  3. 方便添加一些额外的方法,好比加个Print方法打印出各个字段的声明,就能够在UField里加上虚方法,而后在子类里重载实现。

UField名字顾名思义,就是无论是声明仍是定义,均可以看做是类型系统里的一个字段,或者叫领域也行,术语不一样,但能理解到一个更抽象统一的意思就行。

思考:为何UField要继承于UObject?
这问题,其实也是在问,为何类型数据也要一样继承于UObject?反过来问,若是不继承会怎么样?把继承链断开,类型数据自成一派,其实也何尝不可。咱们来列举一下UObject身上有哪些功能,看看哪些是类型系统所须要的。

  • GC,无关紧要,类型数据一开始分配了就能够不释放,当前GC也是利用了类型系统来支持对象引用遍历
  • 反射,略
  • 编辑器集成,也能够没有,编辑器就是利用类型数据来进行集成编辑的,固然当咱们在蓝图里建立函数变量等操做其实也能够看做就是在编辑类型数据。
  • CDO,不须要,每一个类型的类型数据通常只有一份,CDO是用在对象身上的
  • 序列化,必须有,类型数据固然须要保存下来,好比蓝图建立的类型。
  • Replicate,用处不大,由于目前网络间的复制也是利用了类型数据来进行的,类型数据自己的字段的改变复制想来好像没有什么应用场景
  • RPC,也无所谓
  • 自动属性更新,也不须要,类型数据通常不会那么频繁变更
  • 统计,无关紧要

总结下来,发现序列化是最重要的功能,GC和其余一些功能算是锦上添花。因此归结起来无关紧要再加上一些必要功能,本着统一的思想,就让全部类型数据也都继承于UObject了,这样序列化等操做也不须要写两套。虽然这看起来不是那么的纯粹,可是整体上来讲利大于弊。
在对象上,你能够用Instance->GetClass()来得到UClass对象,在UClass自己上调用GetClass()返回的是本身自己,这样能够用来区分对象和类型数据。

总结

UE的这套类型数据组织架构,以我目前的了解和知识,私觉得优雅程度有80/100分。大致上可用,没什么问题,从UE3时代修修改改过来,我以为已经很不容易了。只是不少地方从技术角度上来讲,不是那么的纯粹,好比接口的类型数据也依然是UClass,可是却又不容许包含属性,这个从结构上就没有作限制,只能经过UHT检查和代码中类型判断来区分;又好比UStruct里包含的是UField链表,其实隐含的意思就是UStruct里既能够有嵌套类型又能够有属性,灵活的同时也少了限制,嵌套类型目前是没有了,可是UFunction也只能包含属性,UScriptStruct只有属性而不能有函数;还有UStruct里用UStruct* SuperStruct指向继承的基类。可是UFunction的基Function是什么意义?因此以后若有含糊之时,读者朋友们能够用下面这个图结构来清醒一下:
UFieldAndChildren.jpg-34.9kB
能够简单理解这就是UE想表达的真正含义。UMetaData虽然在UPackage里用TMap<UObject*,TMap<FName, FString>>来映射,可是实际上也只有UField里有GetMetaData的接口,因此通常UMetaData也只是跟UField关联罢了。UStruct包含UProperty,UClass和UScriptStruct又包含UFunction,这才是通常实操时用到的数据关联。

含糊之处固然无伤大雅,只不过若是读者做为一个通用引擎研究开发者而言,也要认识到UE的系统的不足之处,不可一一照抄。读者若是本身想要实现的话,左右有两种方向,一种是向着类型单一,可是更多用逻辑来控制,好比C#的类型系统,一个Type之下能够得到各类FieldInfo、MethodInfo等;另外一种是向着类型细分,用结构来限制,好比增长UScriptInterface来表达Interface的元数据,把包含属性和函数的功能封装成PropertyMap和FunctionMap,而后让UScriptStruct、UFunction、UClass拥有PropertyMap,让UClass,UScriptInterface拥有FunctionMap。都有各自的利弊和灵活度不一样,这里就不展开一一细说了,读者们能够本身思考权衡。
咱们当前更关注是如何理解UE这套类型系统(也叫属性系统,为了和图形里的反射做区分),因此下篇咱们将继续深刻,了解UE里怎么开始开始构建这个结构。

上篇:类型系统概述

引用

  1. UnrealPropertySystem(Reflection)
  2. 虚幻4属性系统(反射)翻译 By 风恋残雪
  3. Classes
  4. Interfaces
  5. Functions
  6. Properties
  7. Structs

UE4.14.2


知乎专栏:InsideUE4
UE4深刻学习QQ群:456247757(非新手入门群,请先学习完官方文档和视频教程)
微信公众号:aboutue,关于UE的一切新闻资讯、技巧问答、文章发布,欢迎关注。
我的原创,未经受权,谢绝转载!

相关文章
相关标签/搜索