ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

UE4类型系统构建全解析:从UHT到UClass的反射原理

UE4类型系统构建全解析:从UHT到UClass的反射原理 好问题这正是国内UE4教程里极少被讲透的一块硬骨头。我不打算重复官方文档里的类图也不打算贴那种“UObject继承自UStructUStruct继承自UField”的老生常谈。我想做的是带你沿着一个类从你敲下UCLASS()宏的那一刻开始一步步看它如何被编译器前端的扫描工具拆解、如何被生成代码记录、如何在引擎启动时被组装成运行时的UClass、再到这个UClass如何被NewObject、蓝图、序列化、GC这些系统当作“数据库字典”来查。我尽量做到“完全构建过程”这四个字名副其实编译前的UHT扫描、生成的中间代码、模块加载时的静态注册、运行时的类型对象创建、CDO的实例化逻辑、以及当这套系统出现异常时该怎么顺藤摸瓜去排查。这篇东西更适合已经写过一阵子UE4 C、遇到过“编辑器里看不到属性”或者“动态创建类失败”这类问题、想彻底搞懂底层机制的开发者。如果你还没写过几个Actor建议先动手写几个带UPROPERTY的类再来读。1. 先搞清楚一个问题UE4为什么非要自己造一套类型系统1.1 标准C没有的反射能力游戏业务却处处依赖很多人第一次接触UE4会有一个困惑明明C已经提供了RTTI运行时类型识别为什么UE4还要搞一套UClass、UObject、UStruct这样自成体系的东西因为标准C的RTTI实在是太“简陋”了。typeid拿到的只是一个类型名称字符串你无法遍历一个类的所有成员变量不知道它们是什么类型、是否可以被编辑器编辑、是否应该被序列化、是否参与网络复制。而游戏引擎需要的恰恰是这些能力——编辑器Detail面板要能自动生成属性编辑控件存档系统要能自动把对象的所有UPROPERTY成员保存到磁盘网络系统要能自动找到需要同步的属性蓝图虚拟机要能在运行时读写C对象里那些标记过的字段。标准C做不了这件事所以引擎就自己造了一套建立在C之上、但比C RTTI强大得多的类型系统。这套系统的核心不是某个单一的代码模块而是“编译期的元数据导出 运行时的类型对象构建”这样一个跨阶段的完整链路。你写下的每一行UPROPERTY(EditAnywhere) float HP实际上是在给这套系统“喂数据”。1.2 UObject、UClass、UStruct三者到底是怎么嵌套的在进入构建过程之前必须先把类型系统里的几个核心类型的关系理顺。我知道很多人背过这张图UObject - UField - UStruct - UClass但这张图背后隐含的设计意图才是关键。UObject是所有“可以被引擎管理的对象”的基类它提供了名字、ID、GC支持、网络复制支持等基础能力。UField是从UObject分出来的一层代表“在类型系统中作为一个字段存在的对象”——无论是UStruct一个结构体或类还是UProperty一个属性、UFunction一个函数它们本身都是一个UObject所以你可以像遍历一个对象列表一样遍历它们这种“自描述”设计是反射系统的基石。UStruct是“拥有子字段的容器”它内部维护了一个Children链表链表的每一项是UField实际上挂着的就是属性FProperty和函数UFunction。UClass则进一步在UStruct基础上增加了父类链SuperStruct、接口列表Interfaces、类默认对象ClassDefaultObject等。理解这一层嵌套后后面讲的“构建过程”你就能看明白了所谓“类型构建”本质上是围绕着UClass这个对象把它内部的属性链表、函数链表、父类指针、接口列表、CDO全部填充完整的过程。构建完成的UClass就像一个充好电的数据库字典随时可以被其他系统查询。1.3 用一句大白话概括整个构建链路如果要一句话概括UE4类型系统的完整构建过程我会这么说UHTUnreal Header Tool在编译前把你的C头文件“翻译”成一份描述类型信息的C代码这份代码在引擎启动、模块加载的时候被调起来创建出运行时的UClass对象并把所有元数据挂到它身上。后面所有使用反射的玩法都是在这个已经建好的UClass上做查询。这个链路里最关键的两个角色分别是编译期的UHT和运行时的StaticClass/Z_Construct_UClass_XXX系列函数。接下来两章我会分别拆开讲。2. 编译期的源头UHT如何把C头文件变成反射元数据2.1 UHT不是正则匹配而是基于Clang的语义级扫描UE4的官方术语里UHT叫“Unreal Header Tool”它运行在编译C代码之前专门扫描带有反射宏UCLASS、USTRUCT、UFUNCTION、UPROPERTY、UENUM、UINTERFACE等的头文件。早期版本的UHT实现方式比较粗糙基本功能够用。但从UE4.21左右开始Epic把UHT重写成了基于Clang库的完整C语法解析器。这意味着UHT真正读懂了你的C头文件它能区分一个UPROPERTY是位于类内还是函数内、能解析出模板参数的类型、能处理复杂的宏展开。这一点很重要因为它解释了为什么有些UE4初学者写的“看似正确”的反射代码会报错。比如你不能在UPROPERTY上标记一个不支持的类型比如TUniquePtrUHT不是简单检查字符串而是真的去解析这个类型然后去它内部的“支持类型白名单”里查。理解了UHT的定位你就能明白它报的错误信息其实都有明确的语义指向。UHT在编译流程中的位置是这样的当你用编辑器或UBTUnreal Build Tool编译一个带有反射类型的模块时UBT会先遍历模块里的头文件把可能包含反射宏的文件列表交给UHT运行UHT分析完后会生成对应的.generated.h和.gen.cpp然后再调用真正的C编译器MSVC/Clang/GCC去编译。生成的.gen.cpp会被一并编译链接进模块里。2.2 生成了什么以UMyActor为例看generated.h的内容我在实际项目里最常看到的现象是很多人知道有.generated.h这么个文件也按照规则把它include在头文件末尾但从不打开看。我强烈建议你干一件事随便建一个UCLASS编译后在Intermediate目录里找到UMyActor.generated.h打开它。你会看到大致这些东西我简化了保留核心结构// UMyActor.generated.h 的核心内容简化示意 #define UMyActor_STATIC_REGISTRATION_MARKER ... // 编译器生成类相关的静态信息 UCLASS() class ENGINE_API UMyActor : public AActor { GENERATED_BODY() public: // 根据属性元数据生成的FProperty描述符 static const TArrayFField** GetMyActor_MetaDataArray(); // 类构造函数的注册入口 static UClass* GetPrivateStaticClass(); // 各类元数据回调 };GetPrivateStaticClass这个名字应该引起你的注意——它是运行时获取这个类的UClass*入口具体实现在.gen.cpp里。.generated.h同时定义了GENERATED_BODY()宏的展开体这个宏在编译时被替换成一组函数声明和字段其中就包括Super别名、StaticClass函数声明、以及编译期标记的静态注册信息。2.3 UPROPERTY和UFUNCTION生成代码的意图解读真正有意思的是.gen.cpp。UE4生成的.gen.cpp并不包含业务逻辑它只做一件事把源代码里的反射声明翻译成运行时可以遍历的数据结构。以属性为例如果你写了UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Stats) float HP;UHT会在生成的.gen.cpp中生成一段注册代码大致逻辑是把HP这个属性描述为一个FProperty对象并给它设置各种元数据标志CPF_Edit可在编辑器改、CPF_BlueprintVisible蓝图可见、归属类名、属性名、偏移量、默认值等。函数也是一样。UFUNCTION(BlueprintCallable)标记的void Fire()会生成一个对应的UFunction对象注册代码这个UFunction内部包含了一个“函数指针”或者说“thunk”蓝图的虚拟机可以通过它间接调用C函数。关键点在于UHT生成的代码不是“运行时解释执行的脚本”而是被编译进二进制的、静态初始化用的C代码。所以反射信息几乎没有任何运行时解析开销它本质上是一个巨大的静态元数据表等引擎启动时把这个表加载进内存而已。2.4 为什么很多新手会漏掉GENERATED_BODY以及后果一个很常见的编译错误也最容易理解UHT工作方式UCLASS() class UMyActor : public AActor { public: UPROPERTY() int32 Test; };如果你忘了写GENERATED_BODY()UHT会直接报错。原因很简单UHT本身并不要求你写这个宏但它生成的.generated.h中定义了很多类需要使用的内联函数和静态数据结构这些内容是通过GENERATED_BODY()宏“注入”到你的类定义中的。没有这行宏注入点就不存在生成的代码与你的类无法形成关联编译自然失败。这个例子可以帮你建立一条重要认知UHT不是读心术它只负责把“可反射的声明”导出为辅助代码而你必须在类的正确位置预留“插槽”GENERATED_BODY()这些代码才能真正嵌入你的类。这一步没配对后面的一切都无从谈起。3. 运行时的构建UClass从无到有的完整路径3.1 模块加载与静态初始化谁在什么时候启动这一切编译期所有生成的静态注册数据已经躺在二进制文件里了但运行时不会自动变成UClass对象。需要一个“点火”过程。这个点火过程发生在模块加载阶段。UE4引擎启动时会加载所有已注册的模块包括项目模块与引擎模块。当你用一个模块比如项目的主游戏模块GameModule时模块的加载函数StartupModule()被执行。在更底层模块加载机制会遍历这个模块里的静态注册表——这个表是在编译期通过IMPLEMENT_MODULE、IMPLEMENT_CLASS这类宏填充的每个注册项指向一个“类构造工厂函数”一般是Z_Construct_UClass_UXXX。如果你去看生成的.cpp代码会看到类似这样的结构简化的示意UClass* Z_Construct_UClass_UMyActor() { // 静态持有保证每个类只构建一次 static UClass* OuterClass nullptr; if (!OuterClass) { // 1. 构建上线文 UObject* Outer Z_Construct_UObject_NoRegister(); // 2. 创建UClass对象 OuterClass UMyActor::GetPrivateStaticClass(Outer); // 3. 设置父类链、接口 // 4. 生成属性链表、函数链表 // 5. 生成CDO } return OuterClass; }这里的Z_Construct_UClass_UMyActor就是运行时类型构建的“心脏”。模块第一次访问某个类型的时候这个函数会被调用类的UClass对象被创建出来并缓存到静态变量里后续再访问直接返回同一个指针。3.2 从UObject到UClass类型对象本身也是一个被管理对象这里必须强调一个关键认知运行时创建出来的UClass本身也是一个UObject。这意味着UClass对象也受UObject体系管理它有名字比如/Script/MyGame.UMyActor、有Outer通常是包/Script/MyGame、可以被GC遍历、可以在日志中打印、可以被FindObject查询。但UClass和普通UObject有一个本质区别它不是一个“实例”而是一个“类型描述”。正因为它继承了UObject我们才能把“类型的类型”串成一条链让类型系统可以自举bootstrapping。例如UClass类的Class是UClass自身的UClass这种自引用关系保证了反射系统在最底层不需要额外解释用同一个机制就能描述引擎自己的类型。实际构建UClass对象时NewObject不会被直接使用而是有专门的StaticConstructObject_Internal内部路径来创建因为UClass在创建过程中需要提前设置好一些普通对象创建时依赖的字段比如ObjectFlags、ClassPrivate等。这些细节你不用完全背下来但你必须知道创建一个UClass对象比创建一个普通UObject要“偏底层”一些。3.3 属性链表与函数链表的挂接细节UClass构建的核心工作量在于把编译期生成的“零散元数据”组装成“可遍历的链表结构”。每个FProperty属性描述对象在.gen.cpp里对应一个NewObjectFProperty的调用同时设置了属性的名字、类型、偏移量、标志位、元数据字典MetaData。UClass构建过程中会逐条读取这些属性描述并把它们链接到UStruct::ChildProperties链表上。为什么用链表而不是数组这是历史原因也是反射遍历的需要。链表让遍历变得非常简单你可以从头到尾走一遍而不用维护动态数组的扩容逻辑。每个UStruct的遍历逻辑比如序列化、编辑器面板生成都是通过这个链表递归处理的先处理自身链表再通过SuperStruct递归处理父类链表。这样子类的FProperty链表上其实不包含父类属性但遍历时会自动上溯父类在逻辑上完成“继承属性”的完整视图。函数链表Children里的UFunction节点的挂接同理。每个UFUNCTION都会生成一个对应的UFunction对象里面有函数名、函数标志BlueprintCallable、BlueprintImplementableEvent、Server等、参数属性链表、返回属性、以及一个指向函数体实现的内部指针如果是C实现。3.4 父类链、接口实现与CDO的构建时机当一个UClass构建时有一件事情必须最先确认它的父类是谁。UMyActor的父类是AActor而AActor的UClass在这个模块加载时通常已经被构建好了因为基础引擎模块先于项目模块加载。构建UMyActor时会通过Z_Construct_UClass_AActor获取父类UClass指针然后设置到SuperStruct字段。接着是接口列表。UINTERFACE宏标记的接口类型也是一类特殊的UClass它包含一组待实现/已实现的UFunction。UClass构建时会把自己声明的接口UClass对象注册到Interfaces数组里同时从父类继承来的接口也会合并进来。所有这些都是“类型字典”的一部分它们共同决定了这个类“是什么、从哪里来、具有哪些能力”。当UClass自身框架搭设完毕后才轮到CDOClass Default Object即类默认对象。CDO的构建是类型系统中容易被忽视但极其重要的一环。每个UClass都有一个独特的默认实例里面保存着所有UPROPERTY的默认构造值。它有两个用途编辑器里“Reset to Default”功能、运行时创建对象时的默认值来源。CDO不是每一次NewObject都复制出来的而是通过“先创建CDO然后普通对象以CDO为模板进行初始化”这种方式工作的。CDO的构建发生在UClass的属性链表、函数链表挂接完成之后。因为它需要读取属性的默认值——UHT生成的元数据里包含属性默认值的信息比如float HP 100.f这些信息在CDO构建时被写入CDO对象的对应内存偏移地址上。3.5 核心流程速查表我把一个UClass从无到有的步骤整理成一张表方便你对照阶段关键动作产物说明编译前UHT扫描头文件.generated.h / .gen.cpp生成静态元数据和构造入口编译时C编译器编译生成代码模块二进制元数据以静态形式存在于二进制中模块加载静态注册表被遍历触发Z_Construct函数第一次访问类型时懒加载类型创建创建UClass对象UClass*设置名字、Outer、父类接口链元数据填充挂接属性链表、函数链表完整UStruct结构序列化、编辑器、蓝图依赖它默认对象构建CDOClassDefaultObject类型级别的默认实例实例化NewObject使用UClass普通UObject实例以CDO为模板初始化这张表可以作为你以后调试类型问题时的心智地图。遇到“编辑器不显示属性”你要查的属性挂接阶段遇到“蓝图里找不到函数”你要查的是函数链表阶段遇到“默认值不对”你要怀疑CDO构建阶段。4. 建好的类型系统在运行时是怎么被“查询”的4.1 StaticClass、GetClass、Cast三者的查询路径类型构建完成后的UClass被缓存起来之后对开发者暴露的最常用入口就是StaticClass()、GetClass()、Cast。StaticClass()是每个UObject派生类自动生成的静态函数它直接返回该类的UClass指针实现里本质是调用GetPrivateStaticClass()也就是前面提到的那个“类构造工厂”。这是获取类型描述最直接、最没有歧义的方式。GetClass()是实例方法返回这个实例对象的UClass指针它读取的是UObject内存中ObjectClass字段。每次NewObject创建对象时这个字段都会被赋值为传入的UClass指针。CastT()则是从“一个UObject实例或UClass”出发沿着UClass的父类链、接口链进行类型判断与转换。它不做字符串比较而是直接走内存中的指针关系判断这也是为什么UE4里Cast比标准C的dynamic_cast更高效——因为它依赖引擎自己维护的UClass结构而非C RTTI。顺带说一句以上三个入口都是线程不安全的至少在早期UE4版本里类型系统构建时不能并发低层次模块在启动阶段访问UClass时要注意这一点。4.2 属性反射序列化、网络复制与编辑器面板的实现基础一旦UClass的属性链表构建完成下游系统就能“无脑遍历”所有属性了。引擎的序列化系统Archive在处理一个UObject时会顺着它的UClass属性链表按顺序把每个FProperty对应的内存数据写入存档或从存档读出。你可以把FProperty想象成一个“知道自己的类型、名字、偏移量、默认值的数据大管家”它内部有SerializeItem、ExportTextItem、ImportTextItem等方法分别处理二进制复制、文本导入导出等场景。网络同步系统也是同样思路FRepLayout类的构建本质上是遍历属性链表把带有Replicated标记的属性提取出来做属性复制和状态同步。这就是为什么网络同步只对UPROPERTY(Replicated)生效——因为UClass构建时只会把带标记的属性挂到“需要同步的属性列表”里。编辑器的Detail面板也是如此IDetailCustomization虽然做的是更高层的UI定制但它底层的默认属性展示逻辑就是遍历UClass的反射属性根据每个属性的元数据生成对应的SWidget控件。你改了UPROPERTY的Category参数后面板分组立刻变化原因就在于UHT编译后元数据里的Category字段被刷新了。所以你可以这么理解UE4里一切“自动发现类型和成员”的能力全部建立在UClass构建完成后的反射数据结构上。这个数据结构越完整下游自动化能力越强。4.3 UFunction调用机制从蓝图虚拟机和RPC的角度看函数反射函数反射的用法我在前面提过一嘴这里展开讲透。UFunction和FProperty很不同属性表示数据函数表示行为。UFunction内部不仅存了函数名和参数还存了函数体实现入口。对于C实现的函数这个入口是一个“原生函数指针”UFunction::Func对于蓝图实现的函数入口则是蓝图虚拟机字节码的起始位置。先看C调用蓝图的情况——BlueprintImplementableEvent标记的函数C侧的UFunction生成时不提供C实现而是留一个空实现占位。蓝图子类重写这个函数时会往UFunction里写入自定义的字节码。当C侧调用这个UFunction时引擎会进入ProcessEvent发现函数是蓝图实现于是把参数打包进“参数栈”交给蓝图虚拟机执行。等执行完结果写回栈C拿到返回值。反过来蓝图调用C函数BlueprintCallable时函数体实现是C函数指针。蓝图虚拟机执行到CALL_FUNCTION指令会通过UFunction找到C函数指针做一个直接的间接调用参数也是通过“参数栈”传递。网络同步的函数UFUNCTION(Server)、UFUNCTION(Client)等又更进一步ProcessEvent会先检查函数是否有SPM_Server或SPM_Client标记如果有不会直接调用本地实现而是走网络RPC通道把函数名和参数序列化成网络消息发到对端由对端反序列化后在UFunction上再次ProcessEvent这次才真正执行函数体。这一整套机制能成立的根本前提是UFunction和FProperty在这个类型系统里成为了“第一公民”可以被查询、遍历、序列化——没有类型构建这一切都是空谈。4.4 GC为什么也依赖类型系统最后再补一个很多马工程序员容易忽视的场景GC垃圾回收。UE4的GC有一个Reachability Analysis可达性分析阶段它会从根部对象集合出发遍历所有存活对象标记那些还在被引用的对象。但问题是它怎么知道一个UObject内部哪些字段是指向其他UObject的引用答案就在UClass的属性链上。GC在遍历一个对象时会读取它的UClass然后遍历这个UClass属性链表上的每一个FProperty。如果FProperty的类型是TObjectPtr或ObjectPropertyGC就把对应地址上存储的UObject指针取出来加入待访问队列。这个遍历过程称为“引用收集器”ReferenceCollector。这意味着即使你在一个UObject里写了一个AActor* MyActor的裸指针如果没有标记为UPROPERTYGC根本不知道这个引用的存在就不能阻止它被销毁。这也就是“UPROPERTY保护引用、非UPROPERTY可能悬垂”这句话的底层机制。你对“为什么必须给对象指针加UPROPERTY”的所有困惑在理解了GC依赖类型系统做引用遍历后都会迎刃而解。5. 类型系统出问题时怎么排查我踩过的坑和调试经验5.1 现象一编辑器里看不到UPROPERTY属性但编译没报错这是我见过最多的问题也是最能反映类型系统认知水平的一个坑。现象是你给一个C类的成员加了UPROPERTY(EditAnywhere)编译也通过了但打开编辑器Detail面板里就是看不到这个属性。我处理这个问题的排查链路是这样的先确认这个类是UCLASS()而不是纯C类没加UCLASS就不会被UHT处理。确认成员是在类的声明里而不是在cpp文件的匿名命名空间或者函数内部UHT只扫描头文件中的类成员声明。确认这个类的头文件真的被模块的Build.cs里的PublicDependencyModuleNames或者PrivateDependencyModuleNames包含进去。如果你加在了不属于该模块的第三方头文件里UHT可能根本不会扫描到它。最关键的一步删除Intermediate文件夹重新完整编译。这个操作能修复很多UHT生成代码与当前源码不一致导致的“缓存型”问题。如果你按这个顺序排查依然没有解决那就要考虑UHT的“元数据缓存”问题。某些情况下修改头文件后UHT会自增版本号重新生成但如果你用的是IDE外部的第三方构建工具比如不同的编译脚本可能会出现生成代码与源码不同步最稳妥的做法永远是删掉Intermediate/Build下的缓存再全量编译。5.2 现象二运行时动态创建对象失败提示“Class not found”在一个大型项目里模块加载顺序和CDO构建顺序问题会导致这种错误。模块A引用了模块B的类但模块B还没加载或者加载成功后静态注册还没来得及跑那么从模块A尝试NewObject一个模块B的类可能会拿到一个不完整的UClass。我遇到过一次这样的问题项目用了一个非常晚加载的插件模块插件模块里定义了一个自定义Actor类游戏启动时主模块想动态创建这个Actor但创建出来的对象行为异常某些属性默认值全是0而正常情况下应该是100。排查时我怀疑是CDO没有正确初始化。用调试器查看这个UClass的ClassDefaultObject字段发现确实是nullptr。问题出在主模块在StartupModule里过早地访问了这个插件类导致UClass的构建流程第一次触发时父类链上的某些类还没有完成全部初始化。因为UClass构建用了静态变量缓存第一次建的残缺UClass被缓存了下来后面即使模块全部加载完成返回的仍然是那个残缺对象。解决的办法是在模块启动的早期不要跨模块访问还没初始化的类型把动态创建逻辑推迟到游戏初始化阶段比如BeginPlay或者延迟到下一帧。这也是为什么官方推荐在InitializeObjectSystem之后再创建对象的主要原因。5.3 现象三函数在蓝图里搜不到这个问题通常是函数缺少正确的UFUNCTION标记或者函数没有BlueprintCallable/BlueprintImplementableEvent/BlueprintNativeEvent之一。但还有一个隐蔽原因蓝图系统只会在“类构建完成”后的那个UClass里搜索函数。如果你用代码动态创建了一个新类运行时生成的动态类并且动态类生成时没有正确调用FBlueprintCompiler或更新UClass的函数链表蓝图编辑器里自然搜不到。排查办法用obj dump CLassName控制台命令输出这个UClass的函数列表。如果列表里确实没有这个函数回头看生成动态类的源码是不是漏了FKismetCompilerContext的编译步骤。另外要注意函数的BlueprintPure和BlueprintCallable之间不能混用。一个纯函数节点不修改状态的函数在蓝图中不会显示执行线。如果你之前定义的是一个纯函数后来改成非纯函数生成的UFunction标志位会更新但蓝图编辑器里的旧节点可能还残留旧的执行引脚遇到这种情况删除旧节点重新拖一个出来就行。5.4 常用的类型诊断工具我平时调试类型系统问题时会频繁用到这几个手段控制台命令obj list class类名列出该类的所有实例能快速判断一个类是否被正确注册。控制台命令obj dump 对象名dump一个对象的所有UPROPERTY值查看默认值是否生效和CDO的值进行对比。GetPrivateStaticClass打断点如果你想观察一个类什么时候被第一次构建在.gen.cpp对应的函数入口打断点最直接。引擎日志过滤在项目启动时启用类型相关的日志详细输出能看到每个模块加载时注册了多少类、每个类的构建状态。那个obj list命令的输出示例是一个非常好的诊断入口比如Class: MyGame.UMyActor Count: 3 ...这些输出都是直接读取运行时UClass结构的结果如果这里输出异常Count为0或者类名显示为None基本可以把问题锁定在UClass构建阶段。5.5 规范习惯避免让自己的代码破坏类型系统最后总结几条我吃了不少亏才养成的规范供你参考不要在StartupModule里直接NewObject别的模块的类除非你能确认依赖模块已完全加载更稳妥的方案是放到一次“后处理”事件里。不要手动修改.generated.h或.gen.cpp。每次UHT重新生成都会覆盖你的改动手动改只会让排查陷入混乱。每次大幅修改反射相关头文件后如果编辑器表现异常先删Intermediate再编译。这个习惯能省掉很多无谓的烦恼。保持“一个类一个头文件”的清晰结构避免在一个头文件里堆大量反射类这会显著降低UHT处理效率和错误排查难度。我个人在实际操作中体会比较深的一点是UE4类型系统构建过程并不难但它处在“引擎源码、编译工具链、运行时初始化”三者的交汇点上一旦出问题排查链路会跨越多个领域。所以把它完全理顺之后你会发现自己对UE4整个引擎的理解会有一个质的提升——你不再只是写代码调API而是能看懂引擎为什么要这样设计、报错到底指的是哪一环。以后遇到反射相关的疑难杂症心里会有一条清晰的排查路径不至于靠猜。最后再分享一个小技巧如果你真的想彻底验证自己对构建过程的掌握程度可以去读一下Z_Construct_UClass_UObject的底层实现再看看UObjectBaseInit相关的初始化代码。把这条最底层的链路读完你就能回答“UE4引擎在运行的最开始是如何把UObject自身这个类型构建出来的”这个问题——那时候再看任何UE4类型系统的资料都会觉得一目了然。
返回列表