手游和活动型项目绕不开热更。UE 的资产热更不神秘:机制就一层——高优先级容器覆盖低优先级容器的同路径文件。难的是把生产流程和兼容红线管住,否则一次热更轻则资源错乱、重则全量崩档。这一篇把机制、流程、红线一次说清。
运行时机制:挂载优先级
FPakPlatformFile(以及 IoStore 的容器挂载)维护一张按优先级排序的挂载表。任何文件路径请求(IFileHandle 层面的 Open)按优先级从高到低问每个容器:"你有 /Game/UI/Icon_A 吗?"第一个答有的赢。于是:
- 基础包装挂时优先级为 0(默认);
- 补丁包挂载时给一个更高的 PakOrder,同路径下补丁内容自动生效,游戏代码完全无感知;
- 同一个包里,补丁没有覆盖的路径继续用基础包的——补丁只需包含变化的资产。
这就是"补丁 = 差量容器 + 高优先级挂载"的全部运行时原理。实现层面注意两点:
- 挂载要在资产被加载之前完成:启动早期(
FCoreDelegates的 pre-init 时机)挂好补丁容器,否则已经加载进内存的资产不会自动换; - 内存里已有的对象不受补丁影响:热更改的是"文件",不是"已加载的 UObject"。要替换运行中的对象,得自己走 Reload 逻辑,或者干脆设计成只影响后续加载。
生产流程:基线 + 补丁两次 Cook
补丁包用 UAT 的补丁模式产出,本质是两次 Cook 的对账:
- 基线:发版那次正常 Cook + Stage,保留 Cook 产物和(Zen)快照作为对账基准;
- 改内容:正常开发迭代;
- 补丁 Cook:UAT 以补丁模式重跑(指向基线的 Cook 元数据),Cook 系统和基线逐包对账,内容哈希变了的包进补丁容器,没变的引用基线数据;
- Stage 补丁:补丁容器单独成 pak/utoc,下发。
下发体积就是第 9060 篇的公式:改动触及的 Chunk 容器体积之和。所以补丁小不光靠"改动少",还靠 Chunk 划法把高频改动隔离在小 Chunk 里。
配套基建一般是:版本服务器存每个补丁容器的清单(包名 + 哈希),客户端启动时比对本地清单,差哪些下哪些,下载完按优先级挂载,进游戏。UE 自带的 Chunk 安装(HTTP chunk install)原理相同,只是下载和挂载由引擎代管。
兼容红线:补丁能改什么、不能改什么
补丁容器加载进的是旧版本的可执行文件。新资产要想被旧代码正确反序列化,格式必须兼容。四条红线,按踩坑频率排:
- 引擎版本必须一致。FPackageFileVersion、CustomVersions、序列化器实现全是 exe 侧代码,补丁资产用的是补丁 Cook 时的格式,和基线 exe 期望的格式必须对得上。升引擎版本必须走整包更新,没有商量余地;
- 不改已发布 UStrUCT/UENUM 的布局。属性按序序列化,结构体中间插入字段、枚举中间插值,都会让旧代码把字节读错位——追加字段放在末尾、枚举新值追加在尾部,才在容错范围内(反射缺失字段有默认值兜底,但错位没有);
- 不改已发布资产的包路径。
FPackageId是包名的哈希(第 9020 篇),改名等于对容器系统声明一个全新资产:旧路径指向的旧包还在基础容器里,新路径的补丁包会和它并存,引用关系全乱; - Cook 配置一致。压缩格式、目标平台设置变了,同路径内容哈希全变,补丁退化成全量(第 9040 篇的键前提)。
反过来,补丁适合改的东西:纹理、材质参数、数据表(DataTable/CurveTable)、UI 资产、关卡布置——纯数据、无新类型、无布局变更的内容。逻辑修复要么走脚本层(Lua/Python 插件),要么老实发整包。
验证与灰度
- 双容器对账:
UnrealPak -List分别列基础容器和补丁容器,确认补丁只含预期包;-fileopenlog跑一局完整流程,确认运行时加载路径都来自预期容器; - 哈希清单校验:下发清单里带每个容器的哈希,客户端挂载前校验,防止下载截断的容器被挂上去——挂载了坏容器的症状是随机缺资源,极难排查;
- 灰度:补丁按渠道/用户分批发,配合
stat pakfile/日志回传,第一批没问题再全量。热更的故障都是分布式故障,发布纪律比技术细节更能救命。
热更的本质是把"文件系统"当成可叠加的层来用。机制一句话,工程上一半的工作在 Cook 对账和 Chunk 划法(9060/9070 篇),另一半在兼容红线——红线踩一次,修复成本就是一次强更,前面省的都赔回去。