1. 什么是 UObject?
在虚幻引擎中,几乎所有由引擎管理的类都继承自 UObject。
标准 C++ 的 new/delete 在 UE 中是被严格限制的。只要一个类继承了 UObject,它就进入了虚幻的"官方管辖区",从而获得了以下超能力:
-
垃圾回收(Garbage Collection):引擎会自动追踪其引用计数,自动释放内存。
-
反射数据支持:其实时元数据(Metadata)可以被引擎在运行时读取。
-
序列化(Serialization):可以轻松地将对象状态保存到硬盘或从硬盘加载。
-
网络复制(Replication):其属性可以通过网络自动同步。
2. 反射系统的核心:UHT 与 4 大宏
由于 C++ 原生不支持反射,UE5 采用了一种预编译生成代码 的方案。这个负责扫描代码的工具叫做 UHT(Unreal Header Tool)。
当你在编写虚幻 C++ 时,必须遵循特定的语法结构。以下是一个标准的 UObject 派生类声明:
#include "CoreMinimal.h"
#include "MyObject.generated.h" // 必须是最后一个 include,由 UHT 自动生成
UCLASS(Blueprintable)
class MYPROJECT_API UMyObject : public UObject
{
GENERATED_BODY() // 虚幻宏,用来注入反射所需的样板代码
public:
// UPROPERTY 让引擎知道这个变量的存在
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
int32 Health;
// UFUNCTION 让引擎和蓝图可以调用这个函数
UFUNCTION(BlueprintCallable, Category = "Actions")
void TakeDamage(int32 Amount);
};
UHT 的工作原理
当你点击编译时,虚幻引擎会先跑一遍 UHT:
-
UHT 扫描你的头文件,一旦发现
UCLASS()、UPROPERTY()、UFUNCTION()、USTRUCT()这四大宏,就会开始解析。 -
UHT 会在后台生成一个对应的
.generated.h和.gen.cpp文件。 -
这些自动生成的文件里,包含了大量的注册代码(如:将
Health变量的名称、类型、内存偏移量注册到引擎全局的反射数据库中)。 -
最后,C++ 编译器(如 MSVC/Clang)才真正介入,把你的代码和 UHT 生成的代码一起编译成二进制文件。
3. 反射在运行时的体现:UClass 与元数据
既然数据被注册了,我们在运行时该怎么用它?
每个继承自 UObject 的类,在运行时都有一个且仅有一个对应的 UClass 对象。这个 UClass 就是该类的"元数据说明书"。
// 获取运行时元数据的方法:
UMyObject* MyObj = NewObject<UMyObject>();
UClass* ClassPrivate = MyObj->GetClass(); // 运行时获取
UClass* ClassStatic = UMyObject::StaticClass(); // 编译时获取(两者指向同一个 UClass)
通过这个 UClass 说明书,你可以做到很多标准 C++ 根本做不到的事。例如遍历类的所有属性:
// 运行时动态遍历 MyObj 的所有成员变量
for (TFieldIterator<FProperty> It(MyObj->GetClass()); It; ++It)
{
FProperty* Property = *It;
FName PropName = Property->GetFName();
FString PropType = Property->GetCPPType();
UE_LOG(LogTemp, Log, TEXT("发现变量名: %s, 类型: %s"), *PropName.ToString(), *PropType);
}
这也是虚幻编辑器(Editor)中"Details(细节)面板"的实现原理:你选中一个 Actor,编辑器通过反射读取这个类的 UClass,发现它有 3 个带有 EditAnywhere 标记的 UPROPERTY,于是就在界面上动态渲染出 3 个输入框。
4. 反射与垃圾回收(GC)的强绑定
作为 C++ 程序员,你一定关心垃圾回收是怎么工作的。虚幻的 GC 是基于标记-清除(Mark-Sweep)算法 的,而它寻找"哪些对象还在被使用"的依据,正是反射。
-
必须被
UPROPERTY()修饰 :如果你的UObject*指针没有加UPROPERTY()宏,反射系统就不知道这个指针的存在。在 GC 扫描时,它会认为这个对象"已经没有人引用了",从而直接将其内存释放。此时你的指针就会变成野指针 (导致引发Access Violation崩溃)。 -
防止内存泄漏的正确姿势:
UCLASS() class AMyActor : public AActor { GENERATED_BODY() // 正确:GC 能看到它,MyObject 存活 UPROPERTY() UMyObject* SafeObject; // 危险:GC 看不到它,MyObject 随时可能被销毁,导致 RawObject 变成野指针 UMyObject* RawObject; };
总结:心智模型转变
| 概念 | 标准 C++ | 虚幻 C++ |
|---|---|---|
| 根基类 | 无 | UObject |
| 内存分配 | new MyClass() |
NewObject<UMyClass>() 或 SpawnActor<AActor>() |
| 生命周期 | 手动 delete 或 std::shared_ptr |
引擎 GC 自动管理(必须挂载 UPROPERTY()) |
| 类型识别 | dynamic_cast (依赖 RTTI) |
Cast<T>() (利用虚幻反射,极其高效) |
| 元数据 | 无 | 通过 UClass 存储,运行时可动态遍历和调用 |