反射与 UHT
自动关联目录:反射与 UHT
反射解决的是一个 C++ 天生做不到的事:让程序在运行时知道自己的类型结构。C++ 没有反射,所以 UE 自己造了一套——办法是在编译前先扫一遍你的头文件,生成描述类型的 C++ 代码。
一句话定位:你写的 UCLASS / UPROPERTY 不是给编译器看的,是给 UHT 看的;编译器看到的是 UHT 生成出来的那份代码。
理解这一点,就能解释本页所有的"为什么不能这么写"。
流水线
你的 .h(带 UCLASS/UPROPERTY 宏)
│ UHT(UnrealHeaderTool)扫描
▼
YourClass.generated.h ──┐
YourClass.gen.cpp ──┤
│ │
└────────────────┴──► 一起交给真正的 C++ 编译器
(UCLASS 宏此时展开为空或普通结构体)所以每次"改了头文件但行为没变",第一反应应该是:UHT 有没有重新跑。
各宏的作用
| 宏 | 作用在 | 生成什么 |
|---|---|---|
UCLASS() | 类 | UClass 对象、CDO、StaticClass() |
USTRUCT() | 结构体 | UScriptStruct(蓝图可见的 POD) |
UENUM() | 枚举 | UEnum |
UPROPERTY() | 成员变量 | 属性描述(名称、类型、偏移、标记) |
UFUNCTION() | 成员函数 | 函数描述 + 蓝图调用的 thunk |
UMETA() | 元数据 | 附加键值(显示名、Tooltip 等) |
GENERATED_BODY() | 类内 | 展开成 UHT 生成代码的接入点 |
没有 GENERATED_BODY(),UHT 生成的代码就接不进来,表现为编译期一堆"找不到 StaticClass"。
UPROPERTY 说明符速查
| 类别 | 常用说明符 | 含义 |
|---|---|---|
| 编辑器 | EditAnywhere / EditDefaultsOnly / EditInstanceOnly | 在哪能改 |
| 可见性 | VisibleAnywhere / VisibleDefaultsOnly | 只读显示 |
| 蓝图 | BlueprintReadOnly / BlueprintReadWrite | 蓝图读写权限 |
| 分类 | Category = "X|Y" | 细节面板分组,支持多级 |
| 网络 | Replicated / ReplicatedUsing = OnRep_X | 参与复制(见 Replication) |
| 资源 | AssetRegistrySearchable | 可被资产注册表检索 |
| 实例化 | Instanced | UObject 属性随外层一起内联创建 |
| 序列化 | SaveGame | 存档时写入(见 序列化与存档) |
| 高级 | Transient | 不序列化、不复制到磁盘 |
| 高级 | Config | 从 ini 读取 |
对 GC 最关键的一条:UPROPERTY() 让这个指针成为 GC 的引用边。漏标 = 没有引用边 = 对象可能被回收(详见 GC 与对象生命周期)。
UFUNCTION 说明符速查
| 说明符 | 含义 |
|---|---|
BlueprintCallable | 蓝图可调用 |
BlueprintPure | 无副作用,蓝图里没有执行引脚 |
BlueprintImplementableEvent | C++ 声明、蓝图实现 |
BlueprintNativeEvent | C++ 有默认实现、蓝图可覆盖(生成 _Implementation) |
Server / Client / NetMulticast | RPC(见 Replication) |
CallInEditor | 编辑器细节面板上出现按钮 |
// BlueprintNativeEvent 的正确写法:实现的是 _Implementation,不是原函数
UFUNCTION(BlueprintNativeEvent)
void OnDamaged(float Amount);
void AMyActor::OnDamaged_Implementation(float Amount) // 注意后缀
{
Health -= Amount;
}直接写 AMyActor::OnDamaged 会链接失败,因为声明已经被 UHT 改写成了 thunk,本体必须是 _Implementation。
头文件写法限制
UHT 是正则式扫描,不是完整的 C++ 解析器。下面是它处理不了的:
| 限制 | 表现 |
|---|---|
一个 UPROPERTY 只能声明一个变量 | UPROPERTY() int A, B; 只认到 A |
不支持模板 UCLASS | 泛型类无法反射 |
不支持 #if 包裹宏(部分版本) | 宏被跳过,属性丢失 |
USTRUCT 不能继承 | 结构体没有继承链 |
构造函数里不能调 NewObject 之外的 UObject API | 此时引擎还没准备好 |
| 类前不能有多余的分号/注释风格 | 偶发解析失败 |
排错口诀:UHT 报错的行号经常不准,真正的问题通常在报错位置往上几行的宏写法上。
运行时怎么用反射
UClass* Cls = MyObject->GetClass();
// 遍历属性
for (TFieldIterator<FProperty> It(Cls); It; ++It)
{
FProperty* Prop = *It;
UE_LOG(LogTemp, Log, TEXT("%s : %s"), *Prop->GetName(), *Prop->GetCPPType());
}
// 按名字取属性并设置值(Int 为例)
if (FIntProperty* IntProp = FindFProperty<FIntProperty>(Cls, TEXT("Count")))
{
IntProp->SetPropertyValue_InContainer(MyObject, 42);
}
// 按名字调函数
UFunction* Func = MyObject->FindFunction(TEXT("OnDamaged"));
if (Func) { MyObject->ProcessEvent(Func, &Params); }TFieldIterator 默认包含父类字段。只想看本类声明的用 EFieldIteratorFlags::ExcludeSuper。
CDO:Class Default Object
CDO 是每个 UCLASS 在引擎启动时自动创建的一个"模板实例",用于:
- 蓝图/编辑器里看到的"默认值"
GetDefault<T>()读配置与默认值- 反序列化时作为基准(只存与 CDO 不同的字段,所以包体更小)
const UMySettings* Defaults = GetDefault<UMySettings>();
float Base = Defaults->BaseDamage;构造函数只应该设置默认值,不要在里面做游戏逻辑——它在引擎启动、编辑器加载、CDO 构造时都会跑,远比你想的频繁。
代价
| 收益 | 代价 |
|---|---|
| 编辑器可见、蓝图可调 | 头文件写法受限,不能自由用 C++ 特性 |
| 自动序列化与网络复制 | 每个 UCLASS 都有一份 UClass 元数据,包体与内存有开销 |
| 自动 GC | 必须理解引用规则,否则对象凭空消失 |
| 热重载与编辑器集成 | 编译前多一遍 UHT 扫描,头文件改动触发范围更大 |
常见坑
| 坑 | 说明 |
|---|---|
忘 GENERATED_BODY() | 一堆 StaticClass 未定义 |
UPROPERTY 声明多个变量 | 第二个不参与 GC / 序列化 |
BlueprintNativeEvent 没加 _Implementation | 链接错误 |
把 UCLASS 写成模板 | UHT 直接忽略 |
| 在构造函数里做游戏逻辑 | 引擎启动时也会执行,可能崩在没初始化的系统上 |
用 UPROPERTY() 修饰 TSharedPtr 管理的非 UObject | 类型不匹配,UHT 报错 |
| 改了宏但没重新编译 | UHT 未重跑,行为与源码不一致 |
EditDefaultsOnly 却想在实例上改 | 说明符选错,实际是可见性类别搞混 |
许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/4fea6af2.html
更新于:2026年10月10日