中EN
概念页

UObject 与反射系统

UObject & Reflection System

更新于 2026-10-12 约 11 分钟UE5UObject反射UHTUPROPERTYUFUNCTIONUCLASS

虚幻引擎 (Unreal Engine) 的对象基类是 UObject。在 C++ 类、属性、函数上加 UCLASS/UPROPERTY/UFUNCTION 等宏,再由构建前的预处理工具虚幻头文件工具 (Unreal Header Tool, UHT) 扫描头文件生成代码,引擎就能在运行时“看见”这些类型——这就是反射系统 (Reflection System)。垃圾回收、序列化、编辑器面板、蓝图、网络复制都建立在它之上。

原理:宏 + 代码生成

C++ 本身没有完整的运行时反射。UE 的做法是:

  1. 开发者用宏给需要暴露的类型和成员打标记,并附加说明符 (Specifier) 和元数据 (Metadata)。
  2. 编译前 UHT 解析头文件,收集这些信息,生成 *.generated.h 等胶水代码。
  3. 运行时每个对象都能通过 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 / NetMulticastRPC:在服务器 / 拥有者客户端 / 服务器与所有客户端执行;实现写在 _Implementation
Reliable / Unreliable只能与 Server 或 Client 搭配
WithValidation额外生成返回 bool 的 _Validate 函数决定是否执行
Exec / CallInEditor控制台命令 / 细节面板按钮
CustomThunkUHT 不生成 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 的兽人生命值例子。详见 世界块数据格式与生成器。
  1. Objects in Unreal Engine UE 5.8 文档
  2. Unreal Engine Reflection System UE 5.8 文档
  3. Unreal Object Handling UE 5.8 文档
  4. Properties (UPROPERTY) UE 5.8 文档
  5. UFunctions UE 5.8 文档
  6. UnrealBuildTool (UBT) & Targets UE 5.8 文档
  7. Game_Engine_Architecture_4th_Edtion_Volume_2_-_Jason_Gregory.pdf
  8. Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 17 章 17.1 节
  9. Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 17 章 17.3.2 节
  10. Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 17 章 17.9.2 节

相关页面

游戏开发 · UE5垃圾回收与对象生命周期Garbage Collection & Object LifecycleUE 用标记-清除式的可达性分析管理 UObject 内存:从根集合 (Root Set) 出发沿反射可见的强引用遍历对象图,找不到的对象在下次垃圾回收 (Garbage Collection, GC) 时被删除。因此“谁持有 UPROPERTY 引用”决定了对象活多久;Actor 调用 Destroy() 只是请求销毁,真正释放仍取决于引用是否全部放开。游戏开发 · UE5蓝图与 C++ 协作Blueprint & C++ InteropEpic 官方建议:多数项目混用蓝图 (Blueprint) 与 C++,以 C++ 作为基础、在其上构建蓝图类。C++ 适合“编程”——定义系统;蓝图适合“脚本”——在已有系统上定义具体行为。两者通过反射说明符互相调用:C++ 用 BlueprintCallable/BlueprintReadWrite 等暴露给蓝图,用 BlueprintImplementableEvent/BlueprintNativeEvent 让蓝图实现或覆盖 C++ 声明的函数。游戏开发 · UE5模块、插件与构建系统Modules, Plugins & Build SystemUE 的代码由模块 (Module) 组成:每个模块是一个独立编译单元,用 [模块名].Build.cs 声明依赖。插件 (Plugin) 是可按项目启用/禁用的代码与内容集合,用 .uplugin 描述,可包含多个模块。虚幻构建工具 (UnrealBuildTool, UBT) 根据 .Target.cs 和 .Build.cs(而不是 IDE 的 .sln)编译,默认把模块编成 DLL 由一个可执行文件加载,也可整体链接成单体 (Monolithic) 可执行文件。游戏开发 · UE5委托与 Tick 机制Delegates & TickUE 有两种驱动游戏逻辑的方式:事件驱动——委托 (Delegate) 在某事发生时回调绑定的函数;轮询驱动——Tick 每帧(或按最小间隔)调用 Actor/组件的更新函数,并按 Tick 组 (Tick Group) 与物理模拟排序。官方建议能用定时器、委托就不用 Tick,必须 Tick 时用 Tick 组和 Tick 依赖保证顺序,避免“差一帧”问题。游戏开发 · UE5Gameplay 能力系统Gameplay Ability SystemGameplay 能力系统 (Gameplay Ability System, GAS) 是 UE 用来构建「Actor 可拥有并反复触发的能力」的框架:能力系统组件 (Ability System Component, ASC) 是持有者,Gameplay 能力 (Gameplay Ability) 编排执行过程,属性集 (Attribute Set) 存数值,Gameplay 效果 (Gameplay Effect, GE) 改数值和标签,Gameplay 标签 (Gameplay Tags) 做状态与互斥,Gameplay Cue 负责表现。它内建复制与客户端预测,代价是概念多、上手重,游戏开发 · UE5MassEntity 数据导向框架MassEntity FrameworkMassEntity 是 UE5 面向玩法的数据导向 (Data-Oriented) 计算框架,属于实体-组件-系统 (Entity Component System, ECS) 一类设计:实体 (Entity) 只是一个 ID 加上一组片段 (Fragment) 数据,相同组成的实体归入同一原型 (Archetype) 并按内存块 (Chunk) 连续存放,无状态的处理器 (Processor) 通过实体查询 (EntityQuery) 批量处理片段。它适合成千上万个同质对象(人群、交通),MassGameplay 插件在其上提供表现、生成、LOD、复制、StateTree 等功能。游戏开发 · UE5网络复制与 RPCNetwork Replication & RPCUE 采用服务器权威 (Server-Authoritative) 的客户端-服务器模型:游戏真正在服务器上进行,服务器把状态复制 (Replication) 给客户端,客户端通过远程过程调用 (Remote Procedure Call, RPC) 向服务器请求操作。开发者需要显式声明哪些 Actor、属性、函数要走网络,并借助所有权、网络角色、相关性、优先级控制“发给谁、发多少”。UE 5.8 提供通用复制系统、Replication Graph 和实验性的 Iris 三套复制系统。游戏开发 · UE5资产管理与异步加载Asset Management & Async LoadingUE 资产默认按引用自动加载:硬引用 (Hard Reference) 会让被引用资产随引用者一起加载,不加控制就可能在启动时加载全部内容。软引用 (Soft Reference,TSoftObjectPtr/FSoftObjectPath) 只存路径,配合流式管理器 (Streamable Manager) 异步按需加载;资产管理器 (Asset Manager) 进一步把内容划分为主资产 (Primary Asset) 和次资产 (Secondary Asset),用资产包 (Asset Bundle) 管理加载、卸载、烘焙分块与内存审计。游戏开发 · UE5Gameplay 框架Gameplay FrameworkGameplay 框架 (Gameplay Framework) 是 UE 提供的一组相互配合的类,用来搭建游戏的基本结构:Actor 与组件 (Component) 构成场景中的一切;GameMode 在服务器上定规则并生成玩家;玩家由“控制器 (Controller) + 棋子 (Pawn)”组成;GameState 与 PlayerState 负责把全局与每个玩家的状态复制给所有客户端;GameInstance 跨关卡存活。游戏开发 · UE5Gameplay 标签深入Gameplay Tags In DepthGameplay 标签 (Gameplay Tags) 是用 . 分层的用户定义标签,必须登记到标签字典(项目设置 .ini、数据表或 C++ 原生标签宏)。对象用 FGameplayTagContainer 持有多个标签并以 HasTag/HasAny/HasAll 判断(注意空集合语义),复杂条件用 FGameplayTagQuery 的叶/根表达式组合并配置在资产里。它是 GAS、AI 和各类玩法状态的通用词汇表;大项目可限制标签编辑权限,并用 IGameplayTagAssetInterface 免 Cast 取标签。游戏开发 · UE5子系统Subsystems子系统 (Subsystem) 是自动实例化、生命周期绑定在宿主对象上的类:Engine、Editor、GameInstance、World、LocalPlayer 五种。宿主创建时自动创建并 Initialize(),宿主关闭时 Deinitialize(),之后无引用即被 GC。它替代了「继承 GameInstance 塞管理器」的老做法,自带按类型的蓝图节点和 Python 访问,特别适合插件。官方编程页只列四种,World 子系统依据 5.8 Python API 补充。游戏开发 · UE5存档系统SaveGameUE 的存档围绕自定义的 USaveGame 子类进行:保存时把需要持久化的数据从游戏世界拷进 SaveGame 对象,再用 UGameplayStatics 写入某个存档槽(槽名 + 用户索引);读档时反过来。官方推荐用 AsyncSaveGameToSlot 异步保存,避免掉帧,也避免某些平台的认证问题。游戏开发 · UE5配置文件系统Configuration FilesUE 的 .ini 配置按类别(Engine、Game、Input……)分层加载:从引擎 Base 文件到项目 Default 文件,再到平台文件和用户文件,后加载的同名键覆盖先加载的。数组可以用 +、-、! 等运算符跨层增删。UCLASS(config=X) 加上 UPROPERTY(Config) 可以让类的默认值从 ini 自动读取;命令行 -ini: 可以临时覆盖。游戏开发 · UE5容器与字符串Containers & StringsUE 不用 STL,而用自带的值类型容器:TArray(连续数组)、TMap/TMultiMap(哈希映射)和 TSet(哈希集合),都假定元素可按字节直接搬移。字符串分三种:FName 存索引、比较便宜,FText 用于可本地化的界面文本,FString 可以修改但开销最大。选错类型的代价主要是性能和本地化。游戏开发 · UE5Live Coding 实时编译Live CodingLive Coding 集成了 Live++,在引擎运行时重新编译 C++ 并给二进制打补丁;它比旧的 Hot Reload 快得多、也更灵活,新安装默认启用,快捷键 Ctrl+Alt+F11。默认开启的「对象重实例化 (Object Reinstancing)」会替换掉现有的 UCLASS/USTRUCT 等对象实例,从而支持新增函数、变量这类结构性修改;关闭它时只适合小改动,大改通常会崩溃。坑:在 .cpp 构造函数里改的默认值不会作用到已存在的实例。游戏开发 · UE5智能指针与 TObjectPtrSmart Pointers & TObjectPtrUE 有两套对象寿命管理:普通 C++ 对象用引用计数智能指针(TSharedPtr/TSharedRef/TWeakPtr/TUniquePtr),UObject 由垃圾回收管理,不能放进这些智能指针。UE5 新增的 TObjectPtr 用来替代 UPROPERTY 里的 UObject 裸指针:编辑器构建中提供动态解析和访问跟踪,非编辑器构建中与裸指针表现相同。游戏开发原型模式Prototype Pattern原型模式 (Prototype Pattern) 的核心思想是“一个对象可以生成与自己相似的对象”:生成器持有一个原型实例,通过 clone() 复制出类和状态都相同的新对象。Nystrom 认为这个设计模式本身在现代游戏中很少是最佳答案,更有价值的是“原型委托 (delegation)”用于数据建模,让数据条目继承另一条目的属性,减少重复。游戏开发资源交叉引用与加载后初始化Resource Cross-References & Post-Load Initialization资源之间以任意方式相互引用(网格引用材质,材质引用纹理),整个资源数据库是一张有向依赖图。内存中的指针离开运行中的程序就失去意义,所以存盘时要把引用改写为 GUID 或文件内偏移,加载后再修正 (fix up) 回指针;含非 POD 的 C++ 对象还要用 placement new 调用构造函数;跨文件的外部引用要先加载全部相关文件。加载后通常还需要按资源类型做加载后初始化(以及释放前的拆除)。游戏开发游戏世界与游戏对象Game World & Game Objects游戏由玩法 (gameplay) 而不是技术定义;游戏机制 (game mechanics) 是支配游戏中各实体交互的规则,以及目标、成败标准、玩家能力、非玩家实体和整体流程(GEA 引用的论文称实现玩法的软件系统为游戏的 G 因子)。大多数游戏世界由静态元素和动态元素组成,划分为若干世界块,并由高层游戏流程组织玩家目标;动态元素通常实现为游戏对象 (GO),引擎提供的建模设施称为游戏对象模型,且分为工具侧和运行时两套(GEA4 卷 II 16、16.1–16.2)。游戏开发世界块数据格式与生成器World Chunk Data Formats & Spawners世界块中的动态部分要为每个游戏对象记录两样东西:属性初始值和对象类型。GEA 比较了三种存储方式:二进制对象映像(生成最简单,但要处理指针、虚表和字节序,且对变化很脆弱)、序列化描述(XML/JSON/私有二进制格式,配合反射与类工厂),以及生成器 (spawner) + 类型模式 (type schema):生成器是只含类型 id 和键值对的轻量数据块,彻底切断世界编辑器与运行时代码的耦合;模式里的默认值配合"与默认值相同就省略键"可以让默认值的修改自动传播到旧实例(GEA4 卷 II 17.3)。游戏开发运行时对象模型架构Runtime Object Model Architectures玩法基础系统 (gameplay foundation system) 是位于"引擎与游戏分界线"附近、供构建游戏规则和动态元素的一组运行时组件,其中最复杂的是运行时对象模型:它在运行时实现世界编辑器展示给策划的抽象对象模型。GEA 把架构分为两大类:以对象为中心(每个工具侧对象对应一个或一小组类实例)和以属性为中心(对象只是一个 id,属性分散在按属性类型划分、以 id 为键的表里)。前者从单体类层次走向组合与组件,后者更省内存、更缓存友好、更易数据驱动,但难以表达属性间关系、也更难调试(GEA4 卷 II 17.1–17.2)。游戏开发享元模式Flyweight Pattern享元模式 (Flyweight Pattern) 把对象的数据拆成可共享的“内在状态”和每个实例独有的“外在状态”,让大量对象共用一份内在状态。森林里成千上万棵树共用一份网格和纹理,以及瓦片地图中每格只存一个指向共享地形对象的指针,都是它的典型应用。综合游戏编程模式选型:解耦与性能Choosing Game Programming Patterns本页把《Game Programming Patterns》里的 19 个模式放到同一张取舍图上:大多数模式要么为了解耦(降低改代码时需要装进脑子里的知识量),要么为了性能(利用对数据和硬件的具体假设),而这两个目标经常互相拉扯。下面先讲全书的定位和 Nystrom 的架构观,再给出按问题选模式的路线和与 UE 的对照。

输入关键词开始搜索

滚轮或双指缩放 · 拖动平移 · 双击放大 / 还原