ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

UE4引擎内部数据解析:从GNames到GObjectArray的底层原理与外部读取

UE4引擎内部数据解析:从GNames到GObjectArray的底层原理与外部读取 1. 为什么要挖这几个引擎内部数据如果你做过一段时间的UE4引擎层开发或者搞过基于UE4的独立工具链肯定绕不开这几个名字UWorld、GNames、GetName、GObjectArray。很多人第一次看到这堆东西的时候完全懵了因为它们既不是常见的蓝图节点也不是普通的C接口而是引擎核心模块里面那几个“底层数据中枢”。先说个小插曲标题里的UWord应该是UWorld的手误不过无伤大雅UWorld本来就是玩家常说的“游戏世界实例”。如果不小心在世界指的层面上把它搞混后面读代码会困惑很久所以先统一一下理解。我做这块分析的出发点很朴素想在不改引擎源码的情况下搞清楚一个运行中的UE4进程里到底有哪些对象、对象叫什么名字、对象之间的层级是怎样的。比如你有一个UWorld对象想从它出发拿到当前关卡的Actor列表或者你有一个UObject指针想看一下它的Name是什么再反查它在全局对象数组中的索引。这些需求在很多场景下都会遇到做引擎编辑器扩展、数据导出插件需要遍历当前世界所有对象做自动化测试框架需要在运行时过滤特定类型的对象做外部分析工具比如抓包查看器、内存核实工具需要通过内存读取出对象树做逆向或安全研究时需要手动模拟引擎内部函数把对象从指针变成可读的字符串。这四样东西形成了一个链路GObjectArray是“所有UObject的仓库”GNames是“名字字符串的仓库”GetName是“从对象到名字的通道”UWorld则是“世界里各类Actor/Component的总入口”。顺着这个链路往下看整个UE4对象体系的地图就清晰了。但实际操作上有一个让人头疼的问题UE4的工程编译出来之后GNames、GObjectArray这类全局变量并不是导出符号。哪怕是开发版本的编辑器也不会把这些内部变量声明到DLL的导出表里给你调用。你写插件、写外部程序理论上拿不到“符号”只能靠签名扫描、特征码匹配、偏移计算等方式去定位。所以这篇文章不会只是贴几个代码片段我会从原理到实操把怎么定位、怎么验证、怎么稳定读取完完整整写一遍。2. GNames全局名称表到底是怎么组织的2.1 FName和FNameEntry的对应关系先说GNames这是所有人接触UE4对象体系时第一个会碰到的东西。UE4里有一个非常基础的类型叫FName它不直接存储字符串而是存储一个索引。这个索引指向全局名称表GNames里的一个条目FNameEntry真正的字符串存在FNameEntry里面。为什么要这样设计直接存字符串不是更简单吗答案是为了高效。UE4里Actor、Component、蓝图路径、资源引用、动画曲线名称、材质参数名等等几乎每个对象都会反复引用相同的字符串。如果每个地方都存一份完整字符串内存浪费非常严重。FName的设计就是“全局只存一份其他地方用索引引用”类似操作系统的字符串驻留string interning机制。FName内部通常有两个索引字段源码里叫ComparisonIndex和DisplayIndex。ComparisonIndex用于比较和哈希DisplayIndex用于显示。绝大多数情况下这两个值是一样的但在某些命名场景下比如带数字后缀的重复对象名可能不同。正常引擎代码中UObject::GetName()返回的是DisplayIndex对应的名称。FNameEntry的结构在不同版本中变化不小。老版本4.20以前的常见形态长这样class FNameEntry { int32 Index; // 该条目在GNames中的索引 TCHAR Name[1024]; // 变长字符串存储 // 后续可能跟随其他hash字段 };新版本4.23之后做了优化不再在条目中保存Index而是根据条目在数组中的物理位置直接计算索引。这意味着GNames的存储结构必须是“固定的、可以通过位置换算索引”的形式实际就是“块数组”结构chunked array分成多个块每个块是一段连续指针数组。这里给个直观的结构图描述GNames └─ TNameEntryArray桶数组每个元素是一个块 ├─ Chunk[0] → 指针数组 → Entry[0], Entry[1], ... ├─ Chunk[1] → 指针数组 → Entry[16384], Entry[16385], ... └─ ...每个Entry里面存的就是TCHAR数组宽字符加一些标记位。要拿某一个索引的名字先根据索引算出它在哪个Chunk再在Chunk内算出它在哪个Entry然后读TCHAR字符串。2.2 版本之间布局差异这部分是真金白银的东西我直接说结论不同版本的GNames区别主要体现在三个地方块大小、Entry头部结构、索引计算方式。块大小在老版本通常是16384即每个Chunk存储16384个FNameEntry指针。新版本有改成更大或更小的还有根据平台不同使用不同值的。具体是多少需要靠扫描内存去验证不能盲信源码。Entry头部结构4.23之前FNameEntry开头有Index字段4.23开始去掉了Index换成了Hash等字段4.25左右结构进一步调整。外部程序读取时要清楚当前目标引擎版本的Entry头部字节数否则字符串偏移全是错的。索引计算方式老版本条目Index直接存储读取时直接用。新版本需要用公式算出索引。假设每个chunk大小是N条目在chunk内是16字节对齐那么对于一个给定的物理地址它在表中的逻辑索引chunkIndex * N elementIndex其中elementIndex根据地址和chunk起始地址的差值除以对齐字节数得到。实操建议以“目标可执行文件的实际版本为准”做一次内存结构确认不要直接在代码里写死。我发现最稳的方式是找几个已知对象比如关卡名、蓝图名做交叉验证先用模式扫描找到GNames起始地址然后按不同版本结构尝试解析看看哪个结构能正确读出预期的字符串。这个验证法很笨但很可靠。2.3 外部程序怎么定位GNames很多人在这一步卡住我知道GNames是全局变量但我怎么拿到它的地址如果引擎是用Development配置编译的PDB符号文件里会有这个变量但实际商用场景下你很难拿到PDB。更通用的方法是“模式扫描”也就是特征码匹配。UE4引擎代码有一定的稳定性GNames附近通常会有一些标志性的字节序列。比如某些引擎版本中GNames全局变量旁边就是几个已知的全局对象入口有些版本则是在某个函数引用之后。简单说模式扫描的流程是读取进程的代码段一般是基址大小比如.text段在内存里挨个位置比对“特征字节串”一旦匹配到就确定某个函数的地址再通过函数内部对全局变量的引用偏移计算出GNames的实际地址。不同引擎版本需要不同的特征码网上各类开源项目里有很多版本的特征码可以抄但是建议自己用IDA或者x64dbg核一遍因为引擎版本细微差别会影响位移。推荐一个调试思路先在本地用编辑器模式跑一个真实项目然后在调试器中查看GNames地址再回到磁盘文件里反向查找是什么指令引用了这个地址。这样你能得到一条很干净的定位路径这条路径再转换成特征码就非常可靠。反复做几个版本之后你会对“引擎二进制镜像布局”有很深的体感后面再遇到新版本速度就快很多。3. GObjectArray全对象数组的分层机制3.1 TUObjectArray与FChunkedFixedUObjectArrayGObjectArray是另一个核心全局变量。它保存了当前进程中所有UObject对象的原始指针UE4内部很多功能都依赖这个数组做遍历和GC标记。从源码角度看其实就是FUObjectArray里面内嵌了一个TUObjectArray而TUObjectArray底层用的是FChunkedFixedUObjectArray。这个FChunkedFixedUObjectArray跟GNames的“块数组”思路类似但存储的不是指针数组而是UObject对象指针的连续块。它分了多个Chunk每个Chunk固定长度比如老的版本是16K或64K个UObject指针对象注册时按顺序放入空闲位置。一个很重要的点这数组里的元素类型是UObject指针本身。也就是说如果外部程序想遍历所有对象只需要拿到GObjectArray的起始地址然后按块读取指针值就行。但要注意这些指针在对象被GC回收后可能悬空遍历时要配合锁或快照机制不然你前一秒读到的指针下一秒就释放了。那么我们怎么确定遍历终止条件答案是元素数量NumElements和最大容量MaxElements。数组内部会维护当前已注册对象总数。外部读取时先把这两个值读出来再按索引范围遍历。切忌一直读到数组物理末尾因为空闲槽位里的值是无效的通常置为nullptr或垃圾数据读取到的“对象指针”一访问就崩。3.2 UObject与GetName的关联在最基础的层面任何一个UObject对象的头部都有一组公共字段UObject基层布局0x00 虚函数表指针 vtable 0x08 对象标志位 ObjectFlags 0x0C 内部索引 InternalIndex 0x14 类私有指针 ClassPrivate 0x20 名称私有指针 NamePrivate 0x28 外层对象指针 OuterPrivateNamePrivate就是FName两个索引字段。所以拿到任意一个UObject指针不用调用任何函数直接读偏移0x20处的两个int32索引再去GNames里查对应的名称字符串即可。这就是GetName函数的核心逻辑只是引擎内部通过虚函数和UObjectBaseUtility封装了一下。UObject::GetName()在引擎内部是这样的调用链UObject::GetName() └─ UObjectBaseUtility::GetName() └─ GetFName().ToString() └─ FName::ToString() → GNames辅助函数 → 找到FNameEntry → 拷贝字符串所以外部程序如果要“模拟GetName”完全不用去找GetName的函数地址直接实现“读对象里的NamePrivate索引 → 查GNames表 → 拿字符串”三步就行。这比Hook函数稳定得多因为不依赖具体函数地址和调用约定。这里补充一个实际经验直接在内存里读UObject偏移时一定要先确认对象基址的虚函数表指针是否合法。很多对象可能是正在构造中的半成品或者已经被GC标记。外层遍历时看到 ClassPrivate 或 NamePrivate 值为0的对象建议直接跳过否则后面解析类名时容易误判。3.3 锁和并发问题既然GObjectArray是全局共享数组那它肯定有并发保护。UE4内部用的是FRWLock读写锁在添加、移除对象时加写锁在遍历时加读锁。外部程序因为是跨进程读写没法直接使用引擎的锁机制所以有两个规避方案暂停目标进程的引擎线程一段时间期间外部进程读取整个对象数组和名称表。这个方案需要具备挂起线程或进程的能力有时候会引发目标进程异常不太推荐。读取快照也就是把GObjectArray的“基址、对象数量、当前状态”一次性读出来然后基于这个快照去做遍历。这个方法不阻塞目标进程但是可能出现部分对象在快照后已被释放的情况所以遍历时每访问一个对象都先验证头部的vtable指针和ClassPrivate是否在一个“可信范围”内过滤无效对象。我倾向于第二种方案配合验证因为项目阶段很多工具只是“数据分析”而不是“实时注入”短暂的一致性问题可以接受。如果你确实需要强一致的快照可以考虑在外部做一个短暂的Suspended状态但那是后话本文先不展开。4. 实战从外部进程视角解析UWorld对象链4.1 拿到GNames和GObjectArray地址实战先从整体流程讲。假定我们面对的是一个Windows平台的UE4进程目标是把UWorld对象解析出来再顺着它找到Level、Actors和每个Actor的Name。第一步当然是定位GNames和GObjectArray。这里我通常写一个简单的模式扫描器步骤如下打开目标进程获取基地址和镜像大小。读取代码段数据。用若干特征码匹配获得某个已知函数地址比如获取对象名的内部辅助函数。在这个函数里寻找对于全局变量的rip相对引用或直接地址引用。将引用偏移计算出来得到全局变量地址。在地址处验证一下GNames起始位置不为0且可按块结构读出合理的字符串GObjectArray地址处有合法的对象指针数组结构和数量字段。代码骨架大致这样这里只写核心逻辑不涉及具体游戏请按自己目标调整特征码bool FindGlobalBySignature( HANDLE process, uintptr_t imageBase, size_t imageSize, const char* pattern, const char* mask, uintptr_t outAddress) { std::vectoruint8_t buffer(imageSize); if (!ReadProcessMemory(process, (LPCVOID)imageBase, buffer.data(), imageSize, nullptr)) return false; // 在buffer中匹配pattern获得匹配偏移 size_t offset MatchPattern(buffer, pattern, mask); if (offset -1) return false; // 基于偏移计算引用的全局变量地址具体方式由指令类型决定 outAddress imageBase offset InstructionRelativeOffset; return true; }实际工程里特征码可以是引擎函数开头的字节序列也可以是一个虚函数表中某些固定slot的调用点。我更习惯用“多段特征码组合”的方式先定位UObject::GetName函数再定位GNames先定位FName::ToString函数再确认名称表地址。这样交叉验证后地址基本不会错。4.2 遍历对象并正确拼名字拿到地址后写一个外部读取类把引擎内存结构映射成我们自己的结构体。需要注意的一大问题是外部进程不能直接解引用目标进程的指针必须通过ReadProcessMemory把值读出来。读取FNameEntry字符串时不能一上来就分配1024个TCHAR慢慢读太慢。应该先读Entry的前几个字节判断字符串长度Entry里存储了Len或某些标记位然后按长度读取字符串缓冲区。这样既快又稳。拼名字的逻辑按这个顺序读UObject指针 ├─ 读ObjectFlags可选用于过滤PendingKill等 ├─ 读NamePrivate两个int32 ├─ 用第一个或第二个索引去GNames查字符串 └─ 返回FName字符串很多新人会踩一个坑FName索引不是直接“从0到N顺序分配”的有些索引位置可能是空槽位或者指向无效Entry。所以查字符串时要做异常处理读不到就返回空或标记为无效对象。有个小窍门很多有名的对象比如“/Engine/Transient”“/Engine/EngineResources/DefaultTexture”都是引擎启动早期注册的索引很小。可以作为API自检的测试基准。4.3 从UWorld出发定位世界演员列表解析UWorld其实还是“偏移计算”。UWorld在内存中的布局在不同版本也有差异但大致上有几个关键字段是稳定的PersistentLevel持久关卡、Levels关卡数组、OwningGameInstance所属游戏实例。从UObject基层出发加上UWorld特有字段之后结构大致是0x00 UObject基础字段vtable, ObjectFlags, InternalIndex, ClassPrivate, NamePrivate, OuterPrivate 0x30 其他UObject成员... 0xXX PersistentLevel指针 0xXX Levels指针TArray含Data和Num 0xXX OwningGameInstance指针遍历Actors的正确起点不是从UWorld的字段去找而是从Level的Actors属性找。Level里有一个TArrayAActor* Actors数组。遍历这个数组就能拿到当前关卡所有的Actor。每个Actor同样有NamePrivate可以用来显示名称和类名也能通过OuterPrivate反查到它属于哪个Level。这里很值得分享一个实际经验不要试图直接从GObjectArray遍历所有对象后通过类名过滤“UWorld”虽然可行但效率低。最优路径是先通过一些已知的静态入口比如引擎全局变量GWorld找到UWorld再从UWorld往下层走。GWorld定位方式和GNames类似都是特征码扫描。GWorld在引擎中的角色是“当前主世界指针”游戏运行时它基本一直有效。找到UWorld之后可以用对象名称做一次验证输出它的名字正常情况应该是“PersistentLevel的外层”或者一些项目自定义的世界名名字对得上就说明整条链路是通的。5. 常见问题与排查技巧实录这里把我实际调试中遇到的高频问题整理一下做成速查表方便你后面遇到类似情况时快速定位。问题现象可能原因排查建议GNames读取出来的字符串全是乱码目标版本Entry结构头大小不对或索引算法错误先确认Entry中Index字段是否存在再调整chunk大小和Entry大小遍历GObjectArray时访问到不合法指针导致崩溃对象已经GC释放或数组元素是nullptr每读一个对象先检查vtable指针和ClassPrivate是否可信跳过无效值对象名称对但是类名解析不对类对象获取偏移错误或ClassPrivate指向的对象没解析先解析ClassPrivate作为UObject读名字确认类对象也在GObjectArray中拿到了UWorld但Levels数组为空引擎版本字段偏移不对或当前世界还不完整如启动加载中把UWorld字段字节打印出来与IDA结构对比不要用固定偏移写死特征码扫描失败引擎版本更新导致函数指令变化用IDA重新定位函数地址换特征码或改用引用模式扫描读取外部进程很慢单次ReadProcessMemory读取太多次小数据按块批量读入本地解析遍历时一次读几百个对象指针暂停目标进程后工具崩溃挂起的是正在持有锁的线程导致死锁改为短暂停快照方式或不要暂停关键线程下面展开几个最值得讲的细节。第一关于GNames的“空槽”问题。FName索引并不是连续的有些索引对应的位置可能没有名字。原因在于引擎会事先保留某些特殊槽位比如空名字、None的表示加上有些构建版本会预分配多余块。遍历时如果发现某个Entry的头部长度变量明显不合理比如大于1024直接跳过。不要试图修复这条数据因为它可能在引擎初始化阶段被留下了标记修复只会污染后续读取。第二GObjectArray里面对象数量会动态变化。外部程序做快照时应该把“对象总数量”和“对象数组最大容量”都读出来。有时候引擎的FUObjectArray内部还可能有“开槽数量”和“已用数量”两个字段以“已用数量”为准。我在一个老版本项目上遇到过“最大容量几百万”但“已用数量只有几万”的情况如果按最大容量遍历白白花几十倍时间。第三版本切换时不要只改“特征码地址”还要改“结构布局”。比如从4.22切到4.25UObject里NamePrivate的偏移可能不变但UWorld里的Levels字段偏移可能变了。所以我的建议是为每个目标引擎版本建一份“布局配置”把UObject、UWorld、GNames、GObjectArray各自的偏移和大小全部固化成常量表。换版本时就只改这个表不碰核心逻辑。这个习惯救了我无数次。第四外部进程读取时一定要做边界校验。目标进程地址空间里的数据是动态的读取的指针可能指向不可读内存。用ReadProcessMemory之前没办法保证指针一定可读所以在封装层要做异常捕获或者先判断指针是否落在模块镜像范围内。对于对象指针来说一般它应该落在目标进程堆区域或特定内存池区域如果指针落在完全不可信区间比如0x0或者未映射区直接丢弃。第五调试这种程序最怕的是“有时候能跑有时候不能跑”。以前我遇到过一个诡异问题同样的代码编辑器模式能读到GNames打包后的游戏读不到。后来发现打包版本里引擎做了代码优化函数被内联特征码完全对不上。解决方式是针对“编辑器二进制”和“打包游戏二进制”分别做特征码配置。类似这种版本差异只能靠多积累多踩坑。再补充一个经验UnrealFinderTool这类开源工具是很好的参照物但不建议直接抄到底。因为它们的代码往往针对特定版本的引擎而且可能已经一两年没更新。更好的方式是看懂它们找GNames和GObjectArray的流程然后自己写一套配置化的版本。6. 代码实现框架与关键段讲解接下来给一个可用的外部读取框架整体是按“配置化布局批量读取”的思路写的。这里只写核心流程实际项目里可以根据目标引擎版本扩展。首先定义布局配置struct FObjectLayout { uint32_t ObjectFlagsOffset; // 通常0x08 uint32_t InternalIndexOffset; // 通常0x0C uint32_t ClassPrivateOffset; // 通常0x14 uint32_t NamePrivateOffset; // 通常0x20 uint32_t OuterPrivateOffset; // 通常0x28 }; struct FGNamesLayout { uint64_t GNamesAddress; uint32_t ChunkSize; // 每个block元素数量 uint32_t EntrySize; // FNameEntry头最大字符串缓冲 bool HasIndexField; // 老版本Entry内是否含Index }; struct FGObjectArrayLayout { uint64_t GObjectArrayAddress; uint64_t ObjectsAddressOffset; // FUObjectArray中TUObjectArray的偏移 uint32_t NumElementsOffset; // 对象数量字段偏移 uint32_t MaxElementsOffset; // 最大容量字段偏移 };然后实现读取FName字符串std::wstring ReadFName(HANDLE process, uint64_t gnames, const FGNamesLayout layout, int32_t nameIndex) { if (nameIndex 0) return L; uint32_t chunkIndex nameIndex / layout.ChunkSize; uint32_t inChunk nameIndex % layout.ChunkSize; // 读Chunk指针 uint64_t chunkPointer 0; ReadProcessMemory(process, (LPCVOID)(gnames chunkIndex * 8), chunkPointer, 8, nullptr); if (chunkPointer 0) return L; // 读Entry指针 uint64_t entryPointer 0; ReadProcessMemory(process, (LPCVOID)(chunkPointer inChunk * 8), entryPointer, 8, nullptr); if (entryPointer 0) return L; // 跳过Entry头读取字符串长度和内容 // 新版Entry在开头有Hash/Len字段实际偏移要看版本 uint32_t nameLen 0; ReadProcessMemory(process, (LPCVOID)(entryPointer layout.EntrySize - 1024), nameLen, 4, nullptr); if (nameLen 1024) return L; std::vectorwchar_t buffer(nameLen 1); ReadProcessMemory(process, (LPCVOID)(entryPointer layout.EntrySize 0), buffer.data(), nameLen * 2, nullptr); return std::wstring(buffer.data()); }再实现遍历GObjectArray读取UObject名称void DumpAllObjectNames(HANDLE process, const FGNamesLayout namesLayout, const FGObjectArrayLayout objLayout) { uint64_t objArrayBase 0; ReadProcessMemory(process, (LPCVOID)(objLayout.GObjectArrayAddress objLayout.ObjectsAddressOffset), objArrayBase, 8, nullptr); int32_t numElements 0; ReadProcessMemory(process, (LPCVOID)(objLayout.GObjectArrayAddress objLayout.NumElementsOffset), numElements, 4, nullptr); if (numElements 0 || numElements 5000000) return; std::vectoruint64_t objPtrs(numElements); // 根据块数组结构分批读取指针 // 这里简化成线性读取实际项目需要分段批量读 for (int32_t i 0; i numElements; i) { uint64_t ptr 0; ReadProcessMemory(process, (LPCVOID)(objArrayBase i * 8), ptr, 8, nullptr); objPtrs[i] ptr; } for (int32_t i 0; i numElements; i) { uint64_t ptr objPtrs[i]; if (ptr 0) continue; // 读UObject公共字段 int32_t nameIndex 0; ReadProcessMemory(process, (LPCVOID)(ptr nameLayout.NamePrivateOffset), nameIndex, 4, nullptr); // 部分版本用的是CompositeIndex低32位即可 std::wstring name ReadFName(process, namesLayout.GNamesAddress, namesLayout, nameIndex); // 继续处理... } }这里要注意线性读取在对象数量很大的时候非常慢建议把读取逻辑改成“按对象逐段预读”而不是逐对象读取。也就是先批量读5000个对象指针再循环处理处理时批量读这5000个对象各自所在内存的前0x100字节把UObject头部的公共字段一次性读出来。这样做速度能提升一个量级。7. 个人经验总结与几个小建议这部分算是我持续性做UE4引擎层分析的几点体会直接放出来供你参考。第一别迷信单一特征码。UE4引擎每个版本的代码布局差异很大同一个函数在4.22和4.25里可能有完全不同的机器码。好的做法是“由一个稳定的行为特征来定位”比如一个始终存在的虚函数表入口、一段稳定字符串的引用点。多段特征码交叉验证比你死磕一段特征码要靠谱得多。第二把引擎源码吃透很重要。你可能觉得外部读取就是“扫描偏移”不需要读源码。但实际上不懂FNameEntry内部结构你就不知道为什么要先读长度再读字符串不懂FUObjectArray的块机制你就不知道为什么不能线性读到底。源码不是只给写游戏逻辑的人看的做分析和工具链的人更要把基础结构搞清楚。第三版本管理一定要做配置化。我在自己的项目里建了一个“引擎版本布局”目录每个版本放一份JSON配置里面包含了GNames、GObjectArray、UWorld、UObject的偏移信息还有一个专门用来跑自检的测试程序。每适配一个新版本就打开这个版本的配置跑一遍自检确认能正确解析“None”“/Script/Engine”这些引擎内置名字然后再进入下一步。这个流程避免了反复返工。第四多利用引擎自身的启动日志和调试输出。有时候解析结果不对你可以先让UE4在启动时打开Log看Log里输出了哪些对象名。用这些Log去对照解析结果能快速判断是偏移出问题还是名字表读取方式有问题。我在UWorld解析阶段最常干的事就是输出UWorld的名字和日志里“WorldContext”相关日志对比名字对上了才继续往下走。最后想分享一个实用的小技巧在外部程序里模拟GetName这个函数时不要只用NamePrivate的ComparisonIndex直接查询。一些对象比如动态生成的Actor的名称可能带下划线和数字后缀这些后缀也不一定都存了单独的FNameEntry。正确做法是先拿DisplayIndex查一次如果发现结果为空再尝试用ComparisonIndex查一次。双索引互补查询能减少很多“名字缺失”的现象。实际上这套分析思路绝不仅限于UWorld、GNames、GetName、GObjectArray这四个点。你把它们吃透后引擎里其他类似的全局数据成员比如GEngine、GConfig、GIsRunning都能用相同的方法去定位。关键是把握“布局-偏移-交叉验证”这条主线后面新版本引擎来了也只是换配置的事。一点经验谈了这么多核心还是希望大家别把UE4引擎当黑盒。它的对象系统虽然庞大但骨架非常清晰所有对象都在一个全局数组里所有名字都在一个全局表里对象与名字之间通过FName索引相连。把这三个事实焊死在脑子里剩下的一切都是时间和耐心问题。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进