UObject 与反射系统
UObject & Reflection System
虚幻引擎 (Unreal Engine) 的对象基类是 UObject。在 C++ 类、属性、函数上加 UCLASS/UPROPERTY/UFUNCTION 等宏,再由构建前的预处理工具虚幻头文件工具 (Unreal Header Tool, UHT) 扫描头文件生成代码,引擎就能在运行时“看见”这些类型——这就是反射系统 (Reflection System)。垃圾回收、序列化、编辑器面板、蓝图、网络复制都建立在它之上。
原理:宏 + 代码生成
C++ 本身没有完整的运行时反射。UE 的做法是:
- 开发者用宏给需要暴露的类型和成员打标记,并附加说明符 (Specifier) 和元数据 (Metadata)。
- 编译前 UHT 解析头文件,收集这些信息,生成
*.generated.h等胶水代码。 - 运行时每个对象都能通过
GetClass()拿到描述自己类型的UClass,其中列出被标记的属性 (UProperty) 和函数 (UFunction)。
下图表示一个带反射的头文件从编写到运行时被各系统使用的流程。
flowchart LR H["MyObject.h<br/>UCLASS / UPROPERTY / UFUNCTION"] --> UHT["UHT 预处理"] UHT --> G["MyObject.generated.h<br/>生成代码"] H --> C["C++ 编译器"] G --> C C --> RT["运行时 UClass<br/>+ 类默认对象 CDO"] RT --> GC["垃圾回收"] RT --> SER["序列化"] RT --> ED["编辑器细节面板"] RT --> BP["蓝图"] RT --> NET["网络复制"]
只有加了说明符宏的属性和函数会出现在 UClass 中;类里仍可以有不参与反射的纯原生成员。
头文件格式要求
#pragma once
#include "CoreMinimal.h"
#include "MyObject.generated.h" // 必须是最后一个 #include
UCLASS()
class MYPROJECT_API UMyObject : public UObject
{
GENERATED_BODY() // UCLASS 与 USTRUCT 都必需
};
| 部分 | 作用 |
|---|---|
#include "X.generated.h" | 必须是文件中最后一个 #include;其他类可前置声明或在它之前包含 |
UCLASS() | 让类对引擎可见,可带类说明符 |
MYPROJECT_API | 需要把类暴露给其他模块时必需,见 模块、插件与构建系统 |
GENERATED_BODY() | 无参数,注入引擎所需的基础设施 |
坑:UHT 只支持 C++ 的一个最小子集。用自定义 #ifdef 包裹 UCLASS 内容时,UHT 会忽略不含 WITH_EDITOR 或 WITHEDITORONLY_DATA 的宏——条件编译的反射成员容易出错。
类默认对象 (Class Default Object, CDO)
每个 UClass 维护一个 CDO:由类构造函数生成、之后不再修改的“默认模板”。CDO 应视为只读。它带来一个实用特性——默认值变更的自动传播:若关卡里已放置的实例某属性值等于旧 CDO 的值,加载时会被更新为新 CDO 的值;若实例改过该值,则保留。官方例子:AEnemy 默认 Health 由 100 改为 150,所有仍为 100 的敌人变为 150,而单独设为 500 的 Enemy_3 保持 500。
对象创建规则
UObject不支持带参构造函数,引擎启动时调用默认构造函数;没有默认构造函数将无法编译。- 构造函数应很轻量,只设默认值和子对象;Actor 与组件的初始化逻辑放到
BeginPlay()。 - 运行时用
NewObject创建,构造函数里用CreateDefaultSubobject创建组件/子对象。 - 永远不要用
new/delete:UObject 由引擎内存管理和垃圾回收,手动管理会破坏内存。 - 对象在构造函数调用前已被整体清零(包括非 UProperty 的原生成员)。
UObject 带来的功能
| 功能 | 说明 |
|---|---|
| 垃圾回收 | 见 垃圾回收与对象生命周期 |
| 引用自动更新 | Actor/组件销毁时,反射可见的引用被自动置空 |
| 序列化 | UProperty 自动读写,除非标记 Transient 或等于默认值;新增属性取新 CDO 默认值,删除的属性被静默忽略;可重写 UObject::Serialize 做版本转换 |
| 编辑器集成 | 属性自动出现在编辑器中,可选择接入蓝图 |
| 运行时类型信息 | Cast<T>() 失败返回空指针,IsA() 查询类型;每个类有 Super typedef 指向父类 |
| 网络复制 | 属性可标记复制,函数可标记为远程执行 |
USTRUCT 具有同样的反射和序列化能力,但是值类型,不参与垃圾回收。
UPROPERTY 常用说明符
语法:UPROPERTY([说明符...], [meta(键=值...)]) 类型 变量名;
| 类别 | 说明符 | 含义 |
|---|---|---|
| 编辑器可编辑 | EditAnywhere / EditDefaultsOnly / EditInstanceOnly | 原型和实例都可改 / 只在原型(类默认值)上改 / 只在实例上改 |
| 编辑器只读 | VisibleAnywhere / VisibleDefaultsOnly / VisibleInstanceOnly | 与 Edit 系列互斥 |
| 蓝图访问 | BlueprintReadOnly / BlueprintReadWrite | 二者互斥 |
| 委托 | BlueprintAssignable | 仅用于多播委托,允许蓝图绑定 |
| 存储 | Transient / SaveGame / Config | 不存盘(加载时清零)/ 纳入存档系统 / 存到 .ini(隐含 BlueprintReadOnly) |
| 网络 | Replicated / ReplicatedUsing=函数 | 复制;更新时回调 |
| 子对象 | Instanced | 每个实例得到默认值中对象的独立副本 |
元数据(如 ClampMin、EditCondition、ExposeOnSpawn、DisplayName)只存在于编辑器,游戏逻辑不得读取元数据。字符串有三种:FString(可变字符数组)、FName(全局字符串表中不可变、大小写不敏感的引用,传递更高效但难操作)、FText(本地化)。
UFUNCTION 常用说明符
| 说明符 | 含义 |
|---|---|
BlueprintCallable | 蓝图可调用 |
BlueprintPure | 不修改对象;const 函数默认为纯函数。纯函数不缓存结果,每次连线求值都会执行,避免做重计算或输出数组 |
BlueprintImplementableEvent / BlueprintNativeEvent | 由蓝图实现 / 蓝图可覆盖且有 _Implementation 原生默认实现,见 蓝图与 C++ 协作 |
Server / Client / NetMulticast | RPC:在服务器 / 拥有者客户端 / 服务器与所有客户端执行;实现写在 _Implementation |
Reliable / Unreliable | 只能与 Server 或 Client 搭配 |
WithValidation | 额外生成返回 bool 的 _Validate 函数决定是否执行 |
Exec / CallInEditor | 控制台命令 / 细节面板按钮 |
CustomThunk | UHT 不生成 thunk,需自己用 DECLARE_FUNCTION/DEFINE_FUNCTION 提供 |
其他反射工具
TSubclassOf<T>:类型安全的UClass引用,常用来让策划在蓝图里选择要生成的武器类。- 接口 (Interface):让不相关的类实现同一组行为。
- UE 智能指针库 (
TSharedPtr/TWeakPtr/TUniquePtr/TSharedRef) 用于非 UObject 的普通 C++ 对象;文档明确智能指针不用于 UObject。 - 构建目标可通过
NativePointerMemberBehaviorOverride禁止 UCLASS/USTRUCT 使用原生指针成员,此时 UHT 报错,应改用TObjectPtr。
取舍与常见坑
- 代价:每个反射类型都要经过 UHT 和生成代码,头文件写法受限;反射只覆盖被标记的成员。
- 不需要这些功能的纯数据或算法类,不必也不应继承 UObject(文档:"并非所有情况都需要或适合")。
- 修改默认值传播依赖“值是否等于旧默认值”:若实例碰巧手动设成了与旧默认值相同的值,也会被当作“未修改”而更新。
GEA 的视角:为什么 C++ 引擎需要自己做反射(卷 II 17.1、17.3.2、17.9.2)
GEA 列出运行时对象模型通常要提供的能力,其中存档与持久化需要 RTTI、反射和抽象构造(不硬编码类名就能创建实例),而 C++ 原生不支持反射(GEA 把 C# 作为反射语言、C/C++ 作为非反射语言的例子),所以引擎必须自己加:
- 反射数据是类内容的运行时描述:类名、数据成员及其类型和偏移、成员函数。有了它就能写通用的序列化系统,而不必让每个类手写
SerializeIn/Out。 - GEA 列举的生成方式包括:用
#define宏包裹数据成员提取信息、由派生类重写返回反射数据的虚函数、为每个类手写反射结构,或其他巧妙办法。本页的UCLASS/UPROPERTY+ UHT 代码生成属于"宏 + 工具生成"这一路(对比归纳)。 - 类工厂:C++ 不能凭名字或 id 实例化类,序列化系统总要有一张"类名/id → 创建函数"的表。UE 中由
UClass承担这一角色(对比归纳)。 - 默认值与健壮性:GEA 推荐的生成器格式是键值对:只存与默认值不同的属性,不认识的键忽略,缺少的键用默认值。本页记录的 UProperty 序列化规则(等于默认值的不写出、新增属性取新 CDO 默认值、删除的属性被静默忽略)与之一致,CDO 默认值的传播也对应 GEA 的兽人生命值例子。详见 世界块数据格式与生成器。
出处
- Objects in Unreal Engine UE 5.8 文档
- Unreal Engine Reflection System UE 5.8 文档
- Unreal Object Handling UE 5.8 文档
- Properties (UPROPERTY) UE 5.8 文档
- UFunctions UE 5.8 文档
- UnrealBuildTool (UBT) & Targets UE 5.8 文档
- Game_Engine_Architecture_4th_Edtion_Volume_2_-_Jason_Gregory.pdf
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 17 章 17.1 节
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 17 章 17.3.2 节
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 17 章 17.9.2 节