反射与 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可被资产注册表检索
实例化InstancedUObject 属性随外层一起内联创建
序列化SaveGame存档时写入(见 序列化与存档)
高级Transient不序列化、不复制到磁盘
高级Config从 ini 读取

对 GC 最关键的一条:UPROPERTY() 让这个指针成为 GC 的引用边。漏标 = 没有引用边 = 对象可能被回收(详见 GC 与对象生命周期)。

UFUNCTION 说明符速查

说明符含义
BlueprintCallable蓝图可调用
BlueprintPure无副作用,蓝图里没有执行引脚
BlueprintImplementableEventC++ 声明、蓝图实现
BlueprintNativeEventC++ 有默认实现、蓝图可覆盖(生成 _Implementation)
Server / Client / NetMulticastRPC(见 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 却想在实例上改说明符选错,实际是可见性类别搞混