Vico Bill< 刘 利 波 > 的个人网站

记录关于学习、工作中的技术点滴

C,C++,Rust,Ruby爱好者;热衷于游戏开发、任务自动化与跨平台;沉迷于游戏引擎与图形表现;深信'简单、多元'哲学的力量。


访问主页

游戏引擎-UE4的类型系统

在多数面向对象的编程语言中,类通常是自省的。何为自省?即,每个类知道自己有多少方法、字段、类变量、类方法以及自己的名字。这有好处,例如 JavaScript、Ruby 中,程序员可以依赖自省来动态添加方法。但对于静态编程语言而言,只有编译器才知晓类的这些信息。自省,有时也可称为反射。反射是程序可以访问、检测和修改其本身状态或行为的能力。C++的哲学是:你不需为你没有使用的功能买单。所有,游戏引擎采用 C++的很大原因是快,因为没有如 Java,C#一样臃肿的运行时。C++不需要运行时,所有的一切都由你来管理。

C++的优点显而易见,但缺点也很明显。C++承自 C 语言,C 语言中容易出错如:越界、内存泄漏、野指针等等,C++为了兼顾性能,并保持 C++简洁性,并没有从语言层面根本解决。例如 C 语言中,内存泄漏和空指针问题,在 C++中提供标准库的 auto_ptr,shared_ptr 等形式解决。所以,C++可以称为 C 语言的微调版本,在 C 语言之上,添加了面向对象、泛型、lambda 表达式等。

对于游戏引擎而言,选用 C++作为开发语言,优势非常明显:高性能、低功耗。但从另一侧面也将开发的难度交给了程序员。不同的程序员,写出的 C++代码质量是很有差别的。优秀的 C++程序员,写出来的 C++代码从各个方面都可达最优水平;而差的 C++程序员,写出来的程序则会拖泥带水、混乱不堪。这种现象会提高门槛,会预先将不合格的、低水平的程序员挡在门外。

UE4 在整个发展历史上,都是行业翘楚,起初的授权费用也是居高不下,以 C++劣汰不合格程序员,提高整体开发素质,在早期是可行的。但随着 UE4 想要从 Unity3D 手中夺取用户,必须得降低门槛、整合功能。UE4 现在是功能全面的游戏引擎,只要想要用到的功能,引擎中都有对应实现。当然,对于编程而言,也要降低门槛,从以下几方面着手:

  • 提供反射机制。便于调试、序列化、创建对象等。
  • 提供垃圾回收。自动内存管理,无需程序员手动管理内存。
  • 提供蓝图,可视化脚本语言
  • 提供代码注释,方便理解用途

在 UE4 中,UE4 对象都继承自 UObject。注意,这里说的 UE4 对象。但也有一部分是不继承自 UObject 的,它们以库的形式存在。UE4 里,这些很好分辨,继承自 UObject 的,都前缀 U(Actor 也是继承自 UObject)。其他类前缀 F 的,则是有其功能的类,并非是 UObject 体系中的类。

这种说法有什么区别呢?UObject 算作是 UE4 中所有对象的基类,如同 C#、Ruby 中,所有类都继承于 Object。继承自 UObject 的类,必有 UObject 提供的功能,那么 UObject 提供了一些什么功能呢?

  • 可以导出到 Blueprint 中
  • 可在编辑器上绘制
  • 内存自动管理,也就是垃圾回收
  • 自省
  • 剖析 等等。UObject 的继承链是这样的:UObject : UObjectUtility : UObjectBase,之后我们会一一分析这几个类,但现在我们从 UObject 提供的优点以及其缺点开始。

如上所述,UObject 丰富了 C++的面向对象系统机制。C++有其设计取舍,它并非完全意义上的面向对象编程语言。完全意义上的编程语言应该如 Ruby 那样,一切皆是对象,所有实例都可自省。像 C#、Java 等常规意义上的面向对象编程语言,都有 Object 基类,UE4 的实现,也是如此。

实现 UObject 意义,在于所有对象,都具有 UObject 的功能,这些功能往往非常基础且重要;其次,类型系统清晰而明确,可调试、可跟踪、可序列化。在 C++中,类型系统通常需要借助 typeid,typeof,typeinfo 等,实现非常原始且不完善的类型功能,在游戏引擎中,它们显得非常鸡肋而常常被禁止使用。再次,可以将继承链所有对象纳入统一的内存管理机制,可以非常容易实现垃圾回收。

那么 UObject 的缺点是什么呢?缺点之一,在所有面向对象编程语言里都是如此:臃肿。UObject 提供的功能越多,越臃肿。继承机制是复制基类的数据,方法也是共用的,如果方法量太多,有时会带来理解上的问题,进而失控,无法对 UObject 进行精确掌握,这对游戏逻辑开发没有多大问题,但如果对性能至关重要的应用场景,那就不得不考虑。

UObject:所有 UE4 对象的基类。

对象的类型由其 UClass 定义,这提供支持函数以创建和使用对象,以及虚函数需要在子类被重载。

首先,我们看下一个宏:DECLARE_CLASS,里面定义了以下几个概念:

  • Super:基类类型
  • ThisClass:此类类型
  • StaticClass():在运行时返回表示此类的 UClass 对象
  • StaticPackage(): 此类所属的包
  • new(): 重载的内存分配函数,内部使用。使用StaticConstructObject()创建新对象。

UObject 的继承链为:UObject:UObjectBaseUtility:UObjectBase,先分析 UObjectBase。

UObjectBase

要探究 UObject 究竟提供什么功能,我们从继承链最顶端开始:UObjectBase。

UObjectBase 拥有如下字段:

private:
    EObjectFlags    ObjectFlags;    // 用于跟踪和报告对象的状态的标记
    int32           InternalIndex;  // 对象在全局对象数组中的索引
    UClass*         ClassPrivate;   // 对象所属的类
    FName           NamePrivate;    // 当前对象的名称
    UObject*        OuterPrivate;   // 居于的对象。
    mutable TStatID StatID;
    mutable PROFILER_CHAR*  StatIDStringStorage;

如上所述,UObject 能自举自身所属类、所属对象、名称、全局索引、标志等。UObject 的方法列表(以伪代码表示):

prot    fun UObjectBase(flags);
pub     fun UObjectBase(class, flags,internalFlags,outer,name); // 构建时会加入到全局表中
pub     fun ~UObjectBase() // 重命名为空,从全局数组中释放
prot:
    fun LowLevelRename(newName,newOuter);// 重新命名并重新计算哈希值
    fun Register(packageName,name); // 注册到包(Outer)中,并添加至全局对象表中。
    fun DeferredRegister(staticClass,packageName,name);
    fun AddObject(name,internalFlags);// 设置对象名称、内部Flags,并对此对象取哈希值

pub:
    fun IsValidLowLevel()
    fun GetUniqueID();//返回全局对象数组中的索引
    fun GetClass(); //对象注册后的所属类
    fun GetOuter(); // 返回所属对象
    fun GetName();  // 返回对象名称
    fun GetStatID(forDeferredUse = false); // 返回剖析ID
priv:
    fun CreateStatID() // 构建完整继承链形成名字以供剖析

prot    fun SetFlagsTo(flags); // 设置标志
pub:
        fun GetFlags();// 返回标志
        fun AtomicallySetFlags(flagsToAdd);// 由异步GC和加载线程调用。原子式设置标志
        fun AtomicallyClearFlags(flagsToClear)

priv    fun SetClass(newClass)

如上所言,UObject 采用 Register 形式将自己注册到包(Outer),并添加至全局对象表中。每个 UObject 对象都有自己的哈希值和剖析统计 ID。全局对象表FUObjectArray GUObjectArray;是对象池数组空间,用于管理对象的分配、回收,此对象表是线程安全的。UObject 通过纳入全局对象表,自动进行内存管理。

UObjectBaseUtility

UObjectBaseUtility 继承自 UObjectBase,为 UObject 提供一些实用方法,因此 UObjectBaseUtility 没有扩展新的数据。在其上扩展了以下几部分内容(伪代码):

//~ 与标志有关
fun SetFlags(flags)
fun ClearFlags(flags)
fun HasAnyFlags(flags)
fun HasAllFlags(flags)
fun GetMaskedFlags(flagsMask)

//~ 与标记有关
fun Mark(marks)
fun UnMark(marks)
fun HasAnyMarks(marks)
fun HasAllMarks(marks)
fun GetAllMarks()

//~ 垃圾回收
fun IsPendingKill()
fun MarkPendingKill()
fun ClearPendingKill()

fun AddToRoot(); // 限定对象不进行垃圾回收
fun RemoveFromRoot()
fun IsRooted()
fun ThisThreadAtomicallyClearedRFUnreachable()
fun IsUnreachable()
fun IsPendingKillOrUnreachable()
fun IsNative()

//~ 内部标志
fun SetInternalFlags(internalFlags)
fun GetInternalFlags()
fun HasAnyInternalFlags(internalFlags)
fun ClearInternalFlags(internalFlags)
fun AtomicallyClearInternalFlags(internalFlags)

//~ 名称有关
fun GetFullName(stopOuter)
fun GetPathName(stopOuter)
fun GetFullGroupName(startWithOuter?)
fun GetName()
fun AppendName()

//~ 垃圾回收集群
fun CanBeClusterRoot()
fun CanBeInCluster()
fun CreateCluster()
fun OnClusterMarkedAsPendingKill()
fun AddToCluster(clusterRootOrObjectInCluster,mutable)
fun @@CreateClusterFromObject(root,refObj)

//~ 外包
fun GetOutermost()
fun MarkPackageDirty()
fun IsTemplate(flags)
fun GetTypedOuter(target)
fun IsIn(outer)
fun IsInA(baseClass)
fun RootPackageHasAnyFlags(mask)

//~ 类
priv fun @@IsChildOfWorkaround(objClass,testClass)

fun IsA(class)

fun FindNearestCommonBaseClass(class)
fun GetInterfaceAddress(interfaceClass)
fun GetNativeInterfaceAddress(interfaceClass)
fun IsDefaultSubobject()

//~ 链接器
fun GetLinker()
fun GetLinkerIndex()
fun GetLinkerUE4Version()
fun GetLinkerLicenseeUE4Version()
fun GetLinkerCustomVersion(versionKey)
fun <(other)

有如下几个术语需要了解:

  • Linker:是与 Unreal 包关联的数据,是磁盘文件与内存中 UPackage 对象之间的桥梁。
  • Class:类的反射数据
  • Package/Outer:包,主要用于保存一系列对象
  • Mark:标记对象的包保存形式
  • Flag:不同的标识,指示 Object 的实例有不同的处理方式
  • Native:C++层面的。Native Class,即 C++类

UObject

UObject 类本身所做的事情并不多,基本上都是 Pre/Post,或 virtual 抽象函数。其主要定义的功能如下:

  • 统一创建对象CreateDefaultSubobject,销毁对象
  • 错误处理
  • 从配置文件加载属性、保存属性到配置文件
  • 获取资源大小占用
  • 序列化
  • 加载、重加载、类重定向加载
  • 跟踪重命名、克隆过程
  • 根据客户端或服务器而加载不同对象
  • 通过事务缓冲,跟踪对象的变化,以便于 Undo/Redo
  • 提供脚本/蓝图的虚拟机环境,并定义脚本内置指令
  • 提供烘焙接口,管理烘焙缓存
  • 跟踪 Linker 改变

综上所述,对于 UObject,你可

  • 管理其整个生命周期:创建、销毁;
  • 可管理其初始化、加载、重定向加载、序列化、保存、修改
  • 可设置标志、标签、标记
  • 可在出错时,进行自定义处理
  • 可管理其 Linker
  • 可自省:类名、基类型、包名、全名、描述;所属类、所属包、子类型、实例列表,实现接口,抽象类地址,接口地址;依赖项,导出/入自定属性、详细信息文本、原型、是否是模版
  • 自身所处 UWorld
  • 如是资源,可获取:大小、引用计数、标签、metadata(显示名称、提示文本、后缀)、基础资源 ID、是否本地化
  • 可通过事务缓冲,对其进行 Undo、Redo
  • 可管理其网络处理:按端加载、收到通知处理、是否可通过网络引用、网络标记销毁、接收网络数据前后处理
  • 可在编辑器中操作:是否选择、修改属性、缓存烘焙管理
  • 可管理脚本虚拟机:函数调用、远程函数调用、控制台命令、取消函数调用等

整个 UObject 的继承链中,UObjectBase 提供数据定义;UObjectBaseUtility 提供成员变量操作函数;UObject 则提供创建对象、补充自省、以及若干接口,以便于实现自定义方法,细粒度控制 UObject 的各个过程。

了解了 UObject,那还需要了解其反射机制所提供的概念。C++本身并没有反射机制,UE4 则补充了这一功能。

编程语言概念的反射数据

UObject/Class.h:

  • UField:反射数据对象的基类
  • UStruct:UFiled : 包含字段的所有 UObject 类型的基类
  • UScriptStruct:UStruct:头部独立数据结构声明或用户定义结构体的反射数据
  • UFunction:UStruct:可复制或可供 Kismet 调用函数的反射数据
  • UDelegateFunction:UFunction:供动态委托声明使用的函数定义
  • UEnum:UField:一个枚举的反射数据
  • UEnumProperty:UField: 枚举属性
  • UClass:UStruct: 一个对象类
  • UDynamicClass:UClass:动态类(能在初始设置之后构造)
  • UInterface:UObject:所有接口的基类
  • UProperty:UField:脚本变量 由上述可见,UE4 实现了完整的反射机制所需要的数据类型系统,在 UObject 体系之下的类型系统,是可以完全无缝使用反射功能。

自动内存管理

与自动内存管理概念相关的,有以下几个类 UObject/GarbageCollection.h:

  • FGCArrayPool:用于降低 GC 分配的池
  • FGCObject:非 UObject 类的 GC 注册对象基类
  • FReferenceCollector:搜集对象的引用
  • FGCReferenceInfo:引用所必需的数据信息
  • FReferenceChainSearch:引用链查找
  • FPurgingReferenceCollector:对象集合的 purge 引用
  • TStrongObjectPtr<T>,TWeakObjectPtr<T>:引用指针
  • FUObjectCluster:将 UObject 编组成单元以便于 GC
  • FUObjectArray:全局对象数组

言到此,似已尽。通篇对 UE4 的类型系统做了简单的回顾。

附:与 UObject 有关的常用内容

traits 特征辨识: TIsInterface<>, TIsCastable<>,TIsPODType<>,

Cast< To,From >, dynamic_cast< To,From >

TSubclassOf< TClass >

GetDefault< TClass >, GetMutableDefault< TClass >

头文件:

ConstructorHelper.h: 对象构造辅助函数

ObjectMacros.h          用于UObject系统的宏和定义

ObjectMemoryAnalyzer.h  内存分析器和内存使用情况

ObjectRedirector.h      对象重定向

ObjectResource.h        资源的导入与导出

ObjectClusters.h        与Cluster相关函数和定义

ObjectGlobals.h         全局可用于Object的公共函数和定义,如异步加载、垃圾回收、加载包、解析包、Duplicate

ObjectHash.h            Object的哈希运算有关

ObjectIterator.h 全局对象数组迭代器

ObjectMarks.h 与 Object 的 mark 有关的函数和定义

最近的文章

【Game】Linux游戏平台-转

分区推荐: 挂载点 分区 大小 / 主分区 20~100G swap 逻辑分区,虚拟内存 2~8G 内存大小的1~2倍 /boot 逻辑分区,引导分区 100~200M /tmp 逻辑分区 1~5G /home 逻辑分区 ...…

继续阅读
更早的文章

【编码】-类型转换剪影

类型转换在编码中,时常会存在。把一个类型变量,赋给另外一个类型变量,会以内存进行适配,即大类型赋给小类型,则多余数据会被丢弃;小类型数据赋给大类型,则大类型多余的部分会是undefined。编程语言中,常见的赋值=运算,实质上是内存数据的COPY。指针或引用的赋值,并不会新分配内存空间,而是将指针的值(地址整数)赋给指针变量,即它们始终指向同一份内存空间。在高级语言中,如js,ruby等,字面量赋值形式,会自动新建内存空间。以变量形式赋值,通常它们引用同一份内存空间。i = 20 # i有...…

继续阅读