技术博客

UE 宏体系:UCLASS/UPROPERTY/UFUNCTION 的语义参数全集

第 4 / 100 篇 · 2025-03-21 · ue5 · 反射UPROPERTY宏说明符

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 位 → 消费系统的三层映射 说明符(源文件层) UCLASS(Blueprintable) UPROPERTY(EditAnywhere) UPROPERTY(Replicated) UFUNCTION(Server) meta=(ClampMin="0") UHT 解析,无冲突检查, 只做部分语义校验 flag 位(ObjectMacros.h) EClassFlags (CLASS_*) EPropertyFlags (CPF_*) EFunctionFlags (FUNC_*) meta= 不进 flag,进 MetaData 键值容器 flag 是位域,上限 64 位; 新说明符越来越多走 meta 消费系统(运行时) 编辑器属性面板 蓝图编译器 / VM FRepLayout 网络复制 序列化 / SaveGame GC 可达性遍历 同一 flag 被多个系统读, 改一处影响全链路

结论

  • 记说明符不如记 flag:遇到问题先在 ObjectMacros.h 里查 flag 位定义,再全文搜谁读这个位,就能看到说明符的真实行为——文档滞后时这是唯一的权威来源。
  • 说明符冲突不报错的居多,行为以 flag 叠加为准;写完重要的 UPROPERTY 组合,去属性面板和蓝图里各看一眼实际表现,比相信 UHT 校验靠谱。
  • 新功能优先走 meta= 路线:flag 位是稀缺资源,Epic 自己都在往 meta 迁移,自定义编辑行为(如自定义 EditCondition)也走 meta 键值。
  • RPC 三件套(Server/Client/NetMulticast)的 _Implementation 是硬性约定,忘了写不报错、静默不执行——这是调试"RPC 没反应"时第一个要查的。

说明符管的是"引擎怎么看待你的代码",而代码内部互相调用靠的是另一套基础设施——委托系统。下一篇拆 DECLARE_DELEGATE 到 TBaseDelegate 的实现。