UE 的反射宏说明符有几百个,官方文档分散在三个页面且经常滞后于源码。这篇按"消费方"重新组织:每个说明符最终被 UHT 翻译成一个 flag 位(EClassFlags/EPropertyFlags/EFunctionFlags,定义在 Runtime/CoreUObject/Public/UObject/ObjectMacros.h),然后被不同系统读取。记住"谁消费这个 flag",比背说明符列表有用得多——查一个说明符的行为,直接去消费它的系统源码里搜 flag 名。
UCLASS:类的身份与暴露面
类说明符翻译成 EClassFlags,消费方主要是编辑器类型系统和蓝图编译器。
| 说明符 | 落点 | 实际效果与坑 |
|---|---|---|
Blueprintable / NotBlueprintable |
CLASS_NotPlaceable 无关,是独立位 |
能不能作为蓝图父类。注意和"能不能放进场景"无关 |
Abstract |
CLASS_Abstract |
不能实例化、不能作为蓝图父类的默认值来源,但可以出现在下拉列表的类型过滤里 |
BlueprintType |
CLASS_CompiledFromBlueprint 相关的可构造位 |
能不能作为蓝图变量类型。struct 上常用 |
Config=Game |
CLASS_Config + 配置文件名 |
标记 config 属性按类节写入 DefaultGame.ini;配置文件名拼错不会报错,只是静默写不进 |
MinimalAPI |
影响模块导出 | 只导出 StaticClass 等最小符号,类成员函数不导出。跨模块用成员函数会链接错误,这是最常见的踩坑点 |
Placeable / NotPlaceable |
CLASS_NotPlaceable |
控制编辑器"放置面板"里出不出现在可拖入列表 |
HideCategories=(...) |
元数据 | 属性面板的分类隐藏,只是显示层过滤 |
meta= 里的 DisplayName、Category 是纯元数据,不参与 flag,编辑器在 UI 层查 UClass::GetMetaData。BlueprintSpawnable(5.x 起部分场景替代 Blueprintable 做组件类)也是元数据路线——新说明符越来越多走 meta 而不是 flag,因为 flag 是位运算有上限,meta 是键值对没上限。
UPROPERTY:序列化、GC、编辑器的交叉点
属性说明符翻译成 EPropertyFlags(CPF_ 前缀),是三类系统共享的开关面板。按消费方分组:
序列化/持久化:
SaveGame:CPF_SaveGame,SaveGame 系统序列化时只挑带这个 flag 的属性。不加它,存档里就没有这个字段。Transient:CPF_Transient,跳过所有序列化。运行时缓存(如材质实例引用)必须加,否则包会越存越大。SkipSerialization:比 Transient 更彻底,连 DDC 导出都跳过。config(小写,配合 UCLASS 的 Config=):CPF_Config,属性值写入 ini。
网络复制:
Replicated:CPF_Net,属性进FRepLayout的 ChangeList。必须配DOREPLIFETIME注册,光写说明符不注册不会同步。ReplicatedUsing=OnRep_Xxx:CPF_RepNotify+ 元数据记回调函数名。回调签名必须是void OnRep_Xxx()或带一个旧值参数,签名错了 UHT 不报错,运行时收不到回调。NotReplicated:在Replicated的基类里排除某个子类属性,用得少但调试复制问题时要知道它存在。
编辑器:
EditAnywhere/VisibleAnywhere:CPF_Edit/CPF_EditConst加可见性位,控制属性面板显示层级(实例、默认值、CDO 三处都能改/都能看)。BlueprintReadWrite/BlueprintReadOnly:CPF_BlueprintVisible/CPF_BlueprintAssignable无关,是可读/可写位。和 EditAnywhere 正交——一个属性可以编辑器可见但蓝图不可见。Category="Combat|Damage":元数据,竖线分层级。不写 Category 的属性在蓝图里归到Default,属性面板里归到类名分类。
元数据(meta=)高频用法:
ClampMin/ClampMax/UIMin/UIMax:属性面板数值框的硬限制和软限制,只影响 UI,不改运行时行为。EditCondition="bEnabled":属性面板按另一个 bool 属性的值灰显,纯 UI 层。AllowPrivateAccess="true":蓝图能访问 private 成员,配合BlueprintReadOnly用。DisplayThumbnail(资源引用类属性):面板里显示缩略图。
UFUNCTION:调用方式与网络语义
函数说明符翻译成 EFunctionFlags(FUNC_ 前缀),消费方是蓝图 VM、网络层和编辑器。
蓝图暴露:
BlueprintCallable:蓝图节点可调。最常用,没坑。BlueprintPure:无执行引脚的数据节点。编译器假定它无副作用,同名 Pure 函数在蓝图里可能被调用多次(每个连接点一次),内部千万别改状态。BlueprintImplementableEvent/BlueprintNativeEvent:C++ 声明、蓝图实现/可覆盖。Native 版本在 C++ 里写_Implementation后缀函数作默认实现。常见坑:NativeEvent 忘了写_Implementation,蓝图不覆盖时调用什么都不发生且不报错。CallInEditor:函数出现在细节面板的按钮上,编辑器里直接点。
网络 RPC:
Server/Client/NetMulticast:FUNC_Net+FUNC_NetServer/NetClient/NetMulticast。函数体必须写_Implementation后缀版本,调用入口是 UHT 生成的 thunk(execXxx),网络层在 thunk 里截获并打包成 RPC Bunch。Reliable/Unreliable:FUNC_NetReliable。Reliable 走有序重传队列,带宽代价大;Unreliable 丢了就丢。移动、输入用 Unreliable,状态切换、伤害结算用 Reliable。WithValidation:5.x 仍保留但已不推荐(安全检查前移到了 Iris),新代码别用。
其他高频:
Exec:控制台命令,SomeCommand arg1直接调。BlueprintAuthorityOnly:蓝图节点只在有网络权威的一端显示可用。CustomThunk:自己写execXxx接管蓝图 VM 的调用帧,做变参或自定义 marshalling 时用(KismetArrayLibrary全是这种)。
冲突与组合:实际行为以 flag 位为准
说明符之间没有语法级的冲突检查,UHT 只做部分语义校验(比如 BlueprintPure 配 Server 会警告)。真正的行为是 flag 位叠加后的结果,几条经验:
EditAnywhere+Transient是合法的,属性面板能改但存盘丢失——做调试参数时很有用,做正式配置时是个坑。BlueprintReadOnly+VisibleAnywhere是"只读暴露"的标准组合;写成BlueprintReadOnly+EditAnywhere蓝图和编辑器行为会不一致(编辑器能改,蓝图不能)。- 复制相关说明符(Replicated/ReplicatedUsing)只在 Actor 及其组件上有意义,写在普通 UObject 上不报错但永远不触发。
meta=(EditCondition=...)引用的属性名拼错,UHT 不校验,运行时静默失效——这类问题用validate命令或引擎自带的UHT 类型检查开关(5.4+ 的bValidateMetaData)能抓到一部分。
结论
- 记说明符不如记 flag:遇到问题先在
ObjectMacros.h里查 flag 位定义,再全文搜谁读这个位,就能看到说明符的真实行为——文档滞后时这是唯一的权威来源。 - 说明符冲突不报错的居多,行为以 flag 叠加为准;写完重要的 UPROPERTY 组合,去属性面板和蓝图里各看一眼实际表现,比相信 UHT 校验靠谱。
- 新功能优先走
meta=路线:flag 位是稀缺资源,Epic 自己都在往 meta 迁移,自定义编辑行为(如自定义 EditCondition)也走 meta 键值。 - RPC 三件套(Server/Client/NetMulticast)的
_Implementation是硬性约定,忘了写不报错、静默不执行——这是调试"RPC 没反应"时第一个要查的。
说明符管的是"引擎怎么看待你的代码",而代码内部互相调用靠的是另一套基础设施——委托系统。下一篇拆 DECLARE_DELEGATE 到 TBaseDelegate 的实现。