前言
虚幻引擎对于 “游戏对象” 有一个健壮的系统来掌控,对象基础的类是UObject
。
可以使用UCLASS
宏来标记从UObject
继承的类,使得虚幻引擎的UObject系统可以识别到它。
还可以使用UPROPERTY
和UFUNCTION
来标记属性和函数,从而实现UObject系统相关的一些功能。
官方文档《Unreal Objects》和《Unreal Object Handling》对UObject系统有了最基础的介绍。虽然有官方翻译,但我想自己再过一遍会加深一些理解。(基本上,这个翻译可以看做是对官方翻译的符合我自己语言习惯的矫正)
UCLASS 宏
UCLASS宏为 UObject 提供一个对 UCLASS 的引用,而UCLASS描述了一个基于虚幻引擎的类型。 每个 UCLASS 都持有一个 类默认对象(Class Default Object) ,简称CDO。CDO 本质上是一个默认的 “模板” 对象,由类的构造函数生成,随后保持不变。尽管UCLASS和CDO通常都会被看做是“只读”的,但是每个Object实例都可以取得他们的UCLASS和CDO。你随时都可以使用 GetClass() 函数来访问Object实例的UCLASS。
UCLASS 包含了一套用于定义这个类的属性和函数——他们就是原生的C++函数与变量,但被虚幻引擎特有的元数据所标记,它们在UObject系统中的行为也因此受到控制。如需了解标记语法的更多细节,请查阅 Programming Reference。
需要注意 UObject 类也可以包含 native-only(译注:应该是指C++原生的,未被标记的属性或函数),这些属性不存在于相应的 UCLASS 中。
属性和函数类型
UObjects 可拥有任意类型的成员变量(称作属性)或函数。然而,为便于虚幻引擎识别并操控这些变量或函数,它们必须以特殊的宏进行标记,并符合一定类型的标准。如需了解这些标准的细节,请查阅 属性 和 UFunction 。
对象创建
有多个函数可用于创建 UObject 实例,包括标准的 new
操作符;他们有其自身的使用情况:
方法 | 描述 |
| 最简单的使用情况。创建新实例的时候提供了可选的参数,拥有所有可用的创建选项。提供极高的灵活性,包含自动生成命名的功能。 |
| 用于在某些底层情况下构造对象,如构建函数需要参数时。 |
UObjects 提供的功能
你不需要在所有情况下都使用该系统(有些情况下甚至不适合使用),但使用这个系统可以得到些好处:
- 垃圾回收
- 引用更新
- 反射系统
- 序列化
- 默认属性变化时自动更新
- 自动的属性初始化
- 自动的编辑器集成
- 运行时类型信息
- 网络复制
对于这些功能的细节,可参见本篇的后半部分。
虚幻头文件工具(UHT)
为利用 UObject 类型所提供的功能,需要在头文件上为这些类型执行一个预处理步骤,以整理需要的信息。该预处理步骤由 UnrealHeaderTool(简称 UHT)执行。
UObject 的类型需要遵守特定的结构,我们也将在此谈到这点。
头文件格式
UObject 在 .cpp文件 中的实现与其他 C++ 类相似,但在 .h文件 中的定义必须遵守特定的基础结构,以便在虚幻引擎中正常使用。使用编辑器的"New C++ Class"命令是设置格式正确的头文件的最简单方法。而 UObject 派生类的基础头文件可能看起来与此相似。假设,我们在 MyProject 项目中创建一个名叫 UMyObject 的 UObject :
#pragma once
#include 'Object.h'
#include 'MyObject.generated.h'
/**
*
*/
UCLASS()
class MYPROJECT_API UMyObject : public UObject
{
GENERATED_BODY()
};
虚幻引擎特定的部分如下依次解释:
#include "MyObject.generated.h"
此行需要为此文件中最后一个 #include 。如此头文件需要了解其他类,可将它们在文件中的任意位置前向声明,或在 “MyObject.generated.h” 的上面进行include。
UCLASS()
UCLASS 宏使虚幻引擎能识别 UMyObject。此宏支持大量的说明符,这些说明符决定了UObject的一些功能的开或关。
class MYPROJECT_API UMyObject : public UObject
如果 MyProject 希望将 UMyObject 类公开到其他模块,则需要添加 MYPROJECT_API 宏。这对于那些将要被游戏项目 include 的模块或插件最有用,他们暴露出了类以跨多个项目提供可移植的(portable)、自包含(self-contained)的功能。
GENERATED_BODY()
GENERATED_BODY 宏不需要参数,但会对类进行设置,以支持引擎要求的基础结构。所有 UCLASS 均有此要求。
更新Object
Ticking 指虚幻引擎中Object的更新方式。所有对象均可在每帧被 tick,以执行必要的更新计算或操作。
Object自己不具有内建的更新能力;但是需要时可以继承FTickableGameObject
来添加更新能力。之后即可实现 Tick()
函数,引擎每帧都将调用此函数。
另外要注意:游戏内多数对象为Actors,可按照用户设置的最低间隔进行 tick(而不是每帧一次的频率进行)。
销毁Object
NOTE
注意,弱指针对于Object是否被垃圾回收,是没有影响的。
Object不被引用后(这意味着没有任何 UPROPERTY 指针、引擎容器或智能指针拥有任何对它的强引用),垃圾回收系统会自动将Object销毁。垃圾回收器运行时,会寻找未引用的Object并将其删除。此外,函数 MarkPendingKill()
可在对象上直接调用,此函数将把所有指向此Object的指针设为 NULL,并从全局搜索中移除对象( remove the Object from global searches)。Object 将在下一次垃圾回收过程中被完全删除。
功能1. 自动属性初始化
UObject
在初始化时,在调用构造函数之前,整个类的UProperties
和原生的成员都会归零。成员随后可以使用构造函数中自定义的值进行初始化。
功能2. 自动更新引用
AActor
或UActorComponent
被销毁或是在运行时删除时,反射系统可见的所有对它的引用(UProperty
指针和虚幻引擎容器类如TArray
中存储的指针)都将自动清空。这样的好处是防止 dangling(译注:指实际指向了一个无效的对象) 指针持久存在并导致后续问题,但也意味着如果其他某段代码将AActor
和UActorComponent
指针销毁,这些指针也会变为空。最终的好处是 判空检查(null-checking) 更可靠,因为“一般情况的空指针” 和 “非空指针指向删除内存”的情况都会被检查出来。
必须要意识到的是,这种功能仅适用于标记了UPROPERTY
或存储在虚幻引擎容器类中的,存储在原始指针中的Object引用对于虚幻引擎是未知的,并且不会自动清空,也不会妨碍垃圾回收。但请注意,这不意味着所有UObject*
变量都必须是UProperties
。如果你需要的Object指针不是UProperty
,请考虑使用TWeakObjectPtr
——它是"弱"指针,意味着不会妨碍垃圾回收,但可以查询Object是否还有效后再访问,并且它所指向的Object要被销毁时,它将被设置为空。
另一种被引用UObject或UProperty自动清空的情况是对编辑器中的Asset使用 “强制删除(Force Delete)”。因此,所有用于处理Asset的UObject的代码,都必须处理他们的指针变为空的情况。
功能3. 序列化
当UObject
被序列化时,所有UProperty
值都将被自动写入或读取(除非显式标记为 “transient” 或无法从 post-constructor 默认值进行更改)。例如,你可以在关卡中放入AEnemy
实例,将其“体力”设置为500,保存并成功地重新加载,实现这个不需要在UClass
定义之外编写任何代码。
当添加或删除UProperties时,系统会无缝处理加载预先存在的内容。新属性从新的CDO复制默认值。被删除的属性将会被默默地忽略。
如果需要自定义行为,则可以对UObject::Serialize
函数进行override。这对于检测数据错误,检查版本号或执行自动转换或更新(在数据格式有变化时)十分有用。
功能4. 更新属性值
当UClass
的 CDO 更改时,引擎将尝试在这个类的实例加载时,应用这些更改。对于一个Object实例,如果更新的变量值与CDO中旧的值相匹配,则就会更新为CDO中新的值。如果变量是其他值,系统会认为这个值是特别设置的,因此这些值将会被保留而不会更新为CDO的新值。
例如,假设你在一个关卡中放置了多个 AEnemy
Object并保存,然后将 AEnemy
构造函数中的默认Health值设置为100。再假设将 Enemy_3 的体力值设置为500。现在,如果你改变注意了,将体力的默认值增加到150。那么下次加载关卡时,虚幻注意到你更改了CDO,就会为那些使用旧默认体力值(100)的所有AEnemy
实例更新为 150。而 Enemy_3 的体力将保持在500,因为它不使用旧的默认值。
功能5. 编辑器集成
编辑器理解UObject
和UProperties
,编辑器可以自动暴露这些值以供编辑,而不必特地编写代码。还可以选择融入蓝图系统。有许多选项可以控制变量和函数的可访问性和暴露情况。
功能6. 运行时类型信息和转换指针类型
由于UObject
是虚幻引擎反射系统的一部分,它们始终知道自己的UClass
是什么,并可以在运行时做出有关类型的判断和指针类型的转换。
在C++代码中,每个UObject
类都有一个Super
被设置为其父类,从而可以轻松控制override行为。示例:
class AEnemy : public ACharacter
{
virtual void Speak()
{
Say("Time to fight!");
}
};
class AMegaBoss : public AEnemy
{
virtual void Speak()
{
Say("Powering up!");
Super::Speak();
}
};
如你所见,调用Speak
将会让MegaBoss说"Powering up!Time to fight!"。
另外,你可以使用模板函数Cast
安全地将Object从基类转换为派生类,或者用IsA
来判断Object是否属于特定类。一个简短示例:
class ALegendaryWeapon : public AWeapon
{
void SlayMegaBoss()
{
TArray<AEnemy> EnemyList = GetEnemyListFromSomewhere();
// The legendary weapon is only effective against the MegaBoss
for (AEnemy Enemy :EnemyList)
{
AMegaBoss* MegaBoss = Cast<AMegaBoss>(Enemy);
if (MegaBoss)
{
Incinerate(MegaBoss);
}
}
}
};
这里我们使用了Cast
来尝试将AEnemy
转换为AMegaBoss
。如果所提及Object实际上不是AMegaBoss
(或者其子类),则Cast会返回空指针,然后我们可以适当做出反应。在以上代码中,Incinerate
函数将仅对MegaBoss类型的对象调用。
功能7. 垃圾回收
虚幻实现了垃圾回收机制,不再被引用或已被显式标记为销毁的UObject
将被定期清除。引擎构建了一个“引用图”以确定哪些UObject
仍在使用,哪些是孤立的。该图的根部是一组指定为 RootSet 的UObject
。任何UObject
都可以添加到 RootSet。当进行垃圾回收时,引擎将从RootSet开始,搜索已知的UObject
引用树来追踪所有引用的UObject
。任何未被引用的UObject
(意味着在引用树搜索中未找到这些对象)将被认为是不再需要,因此被删除。
一个实际的影响是,你通常需要保持一个UPROPERTY
引用指向希望保持活跃的Object,或者将指向它的指针存储在引擎容器类如TArray
中。Actor及其Component通常属于例外情况,因为Actor通常被一个链接到 RootSet 的 Object(例如它们所属的关卡)所引用,而Actor的组件被Actor自身引用。Actor可以显式标记为销毁,方法是调用它们的Destroy
函数,这是在运行时游戏中移除Actor的标准方法。Component可以使用DestroyComponent
函数显式销毁,不过它们通常在拥有它们的Actor从游戏中移除时被销毁。
虚幻引擎中的垃圾回收速度快,效率高,内置大量的优化功能,能够尽量降低开销,如多线程可达性(reachability)分析可以标识孤立的Object,以及优化的 反散列(unhashing) 代码以尽快从容器中移除 Actor。还有一些其他功能以调节,以更精准地控制如何以及何时执行垃圾回收,大部分都可以在 Project Settings 中的 Engine - Garbage Collection 下找到。以下设置通常用于为项目调节垃圾回收器的性能:
设置 | 功能描述 |
创建垃圾回收器UObject集群(Create Garbage Collector UObject Clusters) | 可以在项目设置中打开或关闭(默认打开)。如果打开,相关Object将被分组到一起归入垃圾回收集群,这样只需要检查集群自身即可,而不必检查每个Object。这意味着可以更快速地计算可达性,因为整个集群将被视为一个对象,但也意味着该集群中的单个对象将被反哈希,并准备在同一帧中删除,如果集群足够大,这样可能会导致卡顿。一般而言,集群创建会提高垃圾回收性能,缩短可达性分析耗费的时间。 |
合并GC集群(Merge GC Clusters) | (要让此功能工作,创建垃圾回收器UObject集群(Create Garbage Collector UObject Clusters) 也必须打开。)如果启用合并GC集群(Merge GC Clusters) ,那么当一个集群的对象引用另一个集群的对象时,集群就会合并起来。(请注意,清除导致合并的引用不会让因此合并的集群瓦解或拆散。)这会使垃圾回收器反哈希和销毁对象的频率降低,但一次反哈希和销毁对象的数量会增加。此外,有些情况下不会对合并集群进行垃圾回收,因为对该集群中任何对象的任何引用都会阻止对整个集群进行垃圾回收。 |
启用Actor集群(Actor Clustering Enabled) | 首先要在项目设置中打开这个选项,然后将 |
启用蓝图集群(Blueprint Clustering Enabled) | 蓝图的 |
Time Between Purging Pending Kill Objects | 垃圾回收活动的频率可以在项目设置中调整。该高级控制对于防止卡顿尤其有用。通过缩短回收间隔,可以减少将在下一次可达性分析阶段发现的无法访问的对象的可能数量,并避免同时清除大量Actor时可能会发生的卡顿。 |
功能8. 网络复制
UObject
系统包含一组可靠的功能,能够方便网络通信和多人游戏。
UProperties
可以标记为告诉引擎在网络游戏期间复制数据。常见模型是一个变量在服务器上发生更改,引擎检测到这个更改,并将其可靠地发送到所有客户端。当变量通过复制发生更改时,客户端可以选择性接收回调函数。
UFunctions
也可以标记为在远程机器上执行。例如,"server"函数在客户端上调用时,将会在服务器上执行这个函数以获取服务器版本的Actor。而另一方面,"client"函数可以从服务器调用,并在拥有这个函数的客户端版本的对应Actor上运行。