菜单

注册免费送38元体验金《InsideUE4》UObject(三)类型系统设定和组织

2018年11月13日 - 注册免费送38元体验金

污染源分类,从我做起!

引言

落得篇我们讲到了为什么设计一个Object系统要自品类系统开始做打,并追究了C#的兑现,以及C++中各种方案的相比,最后取得的定论是UE采用UHT的方募集并扭转反射所欲代码。接下来我们就相应初露着手规划真正的种类系统结构。
于后的叙述中,我会以用鲜只观点来考察UE的即时套Object系统:
一样是以一个通用的嬉戏引擎开发者角度来从零开始设计,设想我们正协调实现均等模仿游戏引擎(或者别的需要Object系统的框架),在体悟UE的Object系统的又,思考如何是真的的主干部分,哪些是累的锦上添花。踏出同样条重建Object系统的里程来。
亚凡是坐当下UE4的现状来考量。UE的Object系统由UE3时代就曾是了(再多的UE3有知的长辈还往告知),历经风雨,修修补补,又经UE4的大改造,所以片代码读起来十分是诘屈聱牙,笔者为并无敢说会每一行代码写成那样的原故,只能尽可能从UE的角度去琢磨这样写来什么打算和企图。同时我们呢要是记UE是雅博大精深没错,但连无意味着各一行代码都全面。整体结构及老优雅完美,但也同等来不少不怎么漏洞和短处,也并无是具有的落实还是不过优秀的。所以也支撑读者们以询问的根底及拓展源码改造,符合自己我的开发需要。

PS:类型系统不可避免的说到UHT(Unreal Header
Tool,一个解析源码标记并扭转代码的工具),但以专题不会见详细描述UHT的有血有肉工作流程及公理,只如它万事如我心意,UHT的具体分析后续会发生特定章节讨论。

设定

优先使我们已经接受了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目前莫欲因此并未举行罢了。

结构

当收受了设定之后,是无是当当这个写法有点慌的Hello类看起也产生接触可爱为?

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

先什么都非任,假装UHT已经为我们搜集了周之信息,然后这些消息在代码里该怎么储存?这虽如讲话到一些主干的程序结构了。一个程序,简单来说,可以看是由众底类以及函数嵌套组成的,类型有底蕴项目,枚举,类;类里能够再次定义字段和函数,甚至是子类型;函数有输入和输出,其中间也仍然得以定义子类型。这是C++的条条框框,但若当支持的时光便可以以方进行削减,比如你就足以无支持函数内定义之项目。
预先来探望UE里形成的布局:
注册免费送38元体验金 1
C++有声明与定义的分,图中黄色的底且可作为是声称,而绿色的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里定义了P1、F1、P2、F2,属性与函数交叉在定义,在生成类型A的色数据UClass内部就吧得是坐同等的一一,以后只要想回溯出来一份定义,也足以和原来之代码顺序一致,如果是为此性与函数分开保存的话,就会见烦一些。
  2. 设若齐图可见,所有的甭管是宣称或定义(UProperty、UStruct、UEnum),都得附加一份额外元数据UMetaData,所以当以它们的基类里保存。
  3. 惠及添加一些额外的法,比如加个Print方法打印出各个字段的声明,就得于UField里丰富虚方法,然后于子类里重载实现。

UField名字顾名思义,就是不管是宣称或定义,都可以看成是路系统里的一个字段,或者让世界啊尽,术语不同,但亦可分晓到一个再抽象统一之意就是执行。

考虑:为什么UField要累给UObject?
当下题目,其实为是以讯问,为什么类型数据吧要是同继承给UObject?反过来问,如果未继承会怎么样?把承链断开,类型数据自成一派,其实呢未尝不可。我们来列举一下UObject身上发生哪些职能,看看哪是种系统所急需之。

总下,发现序列化是最最重大之职能,GC和任何有效应算是锦上添花。所以归结起来但有可无论复增长有必需作用,本着统一的考虑,就被具有类型数据为还持续给UObject了,这样序列化等操作也未待写少学。虽然当时看起不是那么的纯,但是完全达成的话利大于弊。
每当目标上,你可据此Instance->GetClass()来取UClass对象,在UClass本身上调用GetClass()返回的是温馨我,这样可就此来分对象与类型数据。

总结

UE的立刻套花色数据组织架构,以我时底垂询及知识,私以为优雅程度有80/100划分。大体上可用,没什么问题,从UE3时代修修改改过来,我当就充分无轻了。只是多地方从技术角度达的话,不是那的纯粹,比如接口的项目数据吧还是UClass,但是也又非容许包含属性,这个由布局及便从来不举行限定,只能通过UHT检查与代码中类型判断来分别;又比如UStruct里带有的是UField链表,其实包含的意就是是UStruct里既然可出嵌套类型又好发性能,灵活的又为不见了限,嵌套类型目前凡是不曾了,但是UFunction也只能分包属性,UScriptStruct只生总体性而非能够发生函数;还有UStruct里用UStruct*
SuperStruct指为继承的基类。但是UFunction的基Function是啊意思?所以后来要有草的时,读者对象等可就此脚这个图结构来清醒一下:
注册免费送38元体验金 2
可以简简单单了解当下就算是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的方方面面新闻资讯、技巧问答、文章发布,欢迎关注。
个人原创,未经授权,谢绝转载!

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图