第三章:Unreal 对象模型:UObject 体系、反射系统、垃圾回收机制
好,咱们今天来聊聊 Unreal 里最核心的东西——对象模型。说白了,就是 UObject 那套体系。很多新手刚接触 UE 时,会觉得这东西有点玄乎,不就是个基类嘛,继承它干嘛?
我刚开始做项目时也这么想。直到有一次,我写了个纯 C++ 类,没继承 UObject,结果发现没法在编辑器里看到它,也没法被蓝图调用,更别提自动释放内存了。嗯,从那以后,我才真正意识到 UObject 这套体系有多重要。
3.1 UObject 体系:一切对象的基石
UObject 是 Unreal 里几乎所有对象的根。你想想看,Actor、Component、Asset,甚至 UWorld、UGameInstance,它们最终都继承自 UObject。为什么?因为 UE 需要一套统一的管理机制。
这套机制包括:
- 反射系统:让 C++ 对象在运行时能被“看穿”
- 垃圾回收:自动管理内存,避免你手动 delete
- 序列化:保存和加载对象状态
- 编辑器集成:属性面板、蓝图节点等
我个人习惯,只要是需要被引擎管理的对象,一律继承 UObject。哪怕只是一个简单的数据容器,我也会用 UObject 而不是普通 C++ 类。为什么?因为后续扩展太方便了。
核心原则:如果你想让一个对象拥有生命周期管理、网络复制、编辑器支持,那就继承 UObject。否则,用普通 C++ 类即可。
3.2 反射系统:让 C++ 拥有“灵魂”
反射,说白了就是让程序在运行时能“认识”自己的结构。普通 C++ 里,你定义了一个类,编译完就变成二进制了,你没法在运行时问它“你有哪些属性?”。但 UE 的反射系统可以。
怎么做到的?靠的是 UE 的 UHT(Unreal Header Tool)。你写的头文件里那些 UCLASS、UPROPERTY、UFUNCTION 宏,UHT 会在编译前扫描它们,生成一堆反射数据。
举个例子:
// MyObject.h
UCLASS(BlueprintType)
class UMyObject : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float Health;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stats")
float MaxHealth;
UFUNCTION(BlueprintCallable, Category = "Stats")
void TakeDamage(float DamageAmount);
};
这段代码里,UCLASS 告诉引擎“这是个 UObject 子类”,UPROPERTY 告诉引擎“这个属性需要暴露给反射系统”,UFUNCTION 告诉引擎“这个函数可以被蓝图调用”。
我在项目中遇到过一个问题:有个同事写了个 UPROPERTY,但忘了加 EditAnywhere,结果美术在编辑器里死活找不到这个属性。排查了半天,其实就是少了个宏。所以,反射宏不是装饰,是实实在在的接口声明。
小技巧:如果你想让一个属性在编辑器里可见但不可编辑,用 BlueprintReadOnly 而不是 EditAnywhere。这样既能展示数据,又防止误改。
3.3 反射系统的核心机制
反射系统到底怎么工作的?我简单梳理一下:
- UHT 预处理:编译前扫描头文件,生成 .generated.h 和 .gen.cpp 文件
- 注册阶段:启动时,所有 UClass 自动注册到全局的 UClass 注册表中
- 运行时查询:通过 UClass 对象,可以获取属性列表、函数列表、父类信息等
- 动态调用:通过 UFunction 的 ProcessInternal 机制,可以动态调用函数
你想想看,这意味着什么?意味着你可以写一个通用工具,遍历任意 UObject 的所有属性,自动生成 UI、自动保存数据、自动网络同步。这就是反射的威力。
我曾经写过一个通用存档系统,就是靠反射遍历所有 UPROPERTY,自动序列化到 JSON。代码量少得惊人,而且完全不需要为每个类单独写序列化逻辑。
3.4 垃圾回收机制:告别手动 delete
C++ 程序员最头疼的是什么?内存泄漏。UE 的垃圾回收(GC)机制就是为了解决这个问题。但它不是 Java 那种全自动 GC,而是基于引用计数的可达性分析。
核心原理很简单:
- 每个 UObject 有一个内部引用计数器
- 当引用计数为 0 时,对象会被标记为待回收
- GC 每帧或每隔一段时间执行一次,清理标记的对象
但这里有个坑:普通 C++ 指针不会被 GC 追踪。你必须用 UPROPERTY 标记,或者用 TObjectPtr、TStrongObjectPtr 等智能指针。
避坑指南:我曾经犯过一个错误——在 UObject 里用普通 C++ 指针指向另一个 UObject,结果那个对象被 GC 回收了,我还傻乎乎地继续用,直接崩溃。记住:UObject 之间的引用,必须用 UPROPERTY 标记。
正确的做法:
UCLASS()
class UMyGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
// 正确:UPROPERTY 标记,GC 会追踪
UPROPERTY()
UMyObject* MyObject;
// 错误:普通指针,GC 不会管
UMyObject* MyObjectUnsafe;
};
3.5 GC 的触发时机与手动控制
GC 不是实时触发的。默认情况下,它会在以下时机执行:
- 每帧结束后(默认频率)
- 显式调用 CollectGarbage()
- 内存压力过大时
我个人建议,在性能敏感的场景下,比如游戏运行时频繁创建销毁对象,可以手动控制 GC 时机:
// 强制立即执行 GC
GetWorld()->ForceGarbageCollection(true);
// 或者延迟几帧再执行
GetWorld()->DelayGarbageCollection(0.5f);
但要注意,频繁调用 GC 会导致卡顿。我见过一个项目,每帧都调用 ForceGarbageCollection,结果帧率直接掉到 20。后来改成每 10 秒一次,问题就解决了。
3.6 对象生命周期管理
除了 GC,UE 还提供了几种生命周期管理方式:
| 方式 | 说明 | 适用场景 |
|---|---|---|
| UPROPERTY 引用 | GC 自动追踪 | 大多数情况 |
| TObjectPtr<> | 现代 UE 推荐的智能指针 | 替代原始 UObject* |
| TStrongObjectPtr<> | 强引用,阻止 GC | 需要确保对象存活 |
| TWeakObjectPtr<> | 弱引用,不阻止 GC | 避免循环引用 |
这里有个经验之谈:能用 TObjectPtr 就别用原始指针。UE5 开始,TObjectPtr 已经成为默认推荐,它比原始指针更安全,而且支持延迟加载。
个人习惯:我在写新代码时,所有 UObject 指针都用 TObjectPtr。只有在需要兼容旧代码或性能极端敏感的地方,才用原始指针。
3.7 总结与避坑清单
好,咱们把这一章的核心点串一下:
- UObject 是根基:需要引擎管理的对象,就继承它
- 反射靠宏:UCLASS、UPROPERTY、UFUNCTION 不是装饰,是接口
- GC 不是万能的:它只追踪 UPROPERTY 标记的引用
- 智能指针更安全:TObjectPtr 是未来趋势
最后,分享几个我踩过的坑:
- 我曾经在构造函数里访问其他 UObject,结果崩溃——因为 GC 还没初始化完成。记住:构造函数里别做复杂操作。
- 我曾经用 TArray<UObject*> 存了一堆对象,结果全被 GC 回收了——因为 TArray 不会自动追踪 UPROPERTY。必须用 UPROPERTY() TArray<TObjectPtr<UObject>>。
- 我曾经在蓝图中创建了一个 UObject,但没保存引用,结果它瞬间被 GC 回收——嗯,蓝图的变量默认是弱引用。
这些坑,你迟早会遇到。提前了解,能省不少调试时间。
下一章,咱们聊聊 Actor 和 Component 的生命周期,以及 Tick 的正确使用姿势。到时候见。