菜单

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

2018年11月17日 - 注册免费送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


知乎注册免费送38元体验金专栏:InsideUE4
UE4深入上QQ群:456247757(非新手入门群,请先念了官方文档和视频教程)
微信公众号:aboutue,关于UE的整个新闻资讯、技巧问答、文章发表,欢迎关注。
私家原创,未经授权,谢绝转载!

相关文章

发表评论

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

网站地图xml地图