在多数面向对象的编程语言中,类通常是自省的。何为自省?即,每个类知道自己有多少方法、字段、类变量、类方法以及自己的名字。这有好处,例如 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 有关的函数和定义