热更新与补丁
自动关联目录:热更新与补丁
热更新的前提是"资源可以覆盖,代码不可以"。UE 的 pak 挂载机制天然支持资源覆盖,所以热更的对象是资产与配置;C++ 逻辑要改,只能换包(或用 Lua 这类脚本层)。
一句话定位:热更新 = 下载更高优先级的 pak → 校验 → 挂载。技术难度不高,难的是版本管理与失败处理。
能力边界
| 能热更 | 不能热更 |
|---|---|
| 贴图、网格、动画、音效 | C++ 代码 |
| 蓝图(作为资产) | 引擎本身的改动 |
| 数据表、配置(ini/Curve/DataTable) | 模块的结构(新增 UCLASS 若被 C++ 引用会出问题) |
| Lua / 脚本(见 UnLua) | 平台层、SDK(见 Platform) |
设计约束:任何"可能会被热更需要改动"的逻辑,一开始就该放在脚本层或数据层。把逻辑写死在 C++ 里,等于放弃热更能力。
完整流程
① 启动 → 读本地版本清单(version.txt / manifest)
② 请求服务器版本 → 比对
③ 有更新 → 下载(全量 pak 或 差分补丁)
④ 校验(文件大小 + 哈希 + 可选签名)
⑤ 写入持久目录(原子写:先 .tmp,再 rename)
⑥ 更新本地版本清单(这一步必须在挂载之前持久化完成)
⑦ Mount pak
⑧ 进入游戏第 ⑥ 步是本流程中唯一必须"先落盘再生效"的地方。如果先挂载再写清单,中途崩溃会导致"代码以为已更新、磁盘上还是旧的",也就是所谓"半更新状态"。
版本管理
| 方案 | 说明 | 适用 |
|---|---|---|
| 单调递增版本号 | 123 → 124,简单可靠 | 推荐 |
| 全量 pak + 版本目录 | 每个版本一个目录 | 简单但下载大 |
| 差分补丁链 | v1→v2→v3 依次打 | 省流量,但链断了就废 |
| 基线 + 累积补丁 | 定期出基线,中间打补丁 | 折中 |
PersistentDir/
Paks/
patch_v120.pak
patch_v121.pak
patch_v122.pak ← 挂载时按版本号倒序,新的优先
version.txt ← 已生效的版本差分补丁的隐患:补丁链越长,中间任一环出错就越难恢复。常见做法是超过 N 个补丁就要求下载全量包。
差分方案
| 工具 | 特点 |
|---|---|
| bsdiff / bspatch | 经典,压缩率高,内存占用大 |
| HDiffPatch(HDiffZ) | 中文文档友好,适合大文件,支持流式 |
| zstd --patch-from | 集成在 zstd 里,速度快 |
| 自研按资产粒度 | 只替换变动的资产,跳过未改动的 |
按资产粒度做差分往往比二进制差分更实用:pak 里资产顺序一变,二进制差分就失效;而按资产对比能稳定识别出"只有这 3 个贴图变了"。
校验
| 层 | 手段 |
|---|---|
| 传输 | HTTPS |
| 完整性 | SHA-256 / MD5(防传输损坏) |
| 来源 | pak 签名(防伪造,见 Pak) |
| 版本 | 清单里带版本号与哈希,双重校验 |
// 写入前校验哈希,不匹配就删掉重新下
FString Hash = FMD5::HashFile(*TmpPath);
if (Hash != ExpectedHash) { IFileManager::Get().Delete(*TmpPath); return false; }
IFileManager::Get().Move(*FinalPath, *TmpPath); // 原子替换永远不要直接下载到目标文件名——中断会产生半截文件,下一次启动会把它当成有效文件。
失败与回滚
| 失败点 | 处理 |
|---|---|
| 下载中断 | 断点续传;失败保留旧版本继续进游戏 |
| 校验失败 | 删除临时文件,重试 N 次,仍失败则用旧版本进游戏(或强更) |
| 写盘失败 | 检查磁盘空间,先检查再下载 |
| 挂载失败 | 记录日志,回退到旧版本 |
| 更新后启动崩溃 | 需要"安全模式":检测到连续崩溃 N 次就跳过更新 pak |
最重要的兜底:让"更新失败"不等于"进不去游戏"。强更(不更新不能玩)要有单独的理由与提示,不要默认开启。
// 崩溃保护:用本地计数值判断
int32 CrashCount = LoadCrashCount();
if (CrashCount >= 3) { SkipMountedPatches(); } // 安全模式强制更新与兼容性
| 情况 | 必须强更 |
|---|---|
| 协议不兼容(服务端已升级) | 是 |
| 存档格式变了且无法前向兼容 | 是 |
| 纯美术资源更新 | 否 |
| 平衡性数值 | 否(可以下次再说) |
判断标准:旧版本客户端继续运行会不会造成数据污染或破坏他人体验。会 → 强更;不会 → 允许延迟更新。
移动端额外事项
| 平台 | 注意 |
|---|---|
| Android | 存储权限、分区存储(Scoped Storage)、OBB 大小限制 |
| iOS | App Store 审核规则:不能下载可执行代码,脚本引擎要小心 |
| 通用 | 后台下载、断点续传、蜂窝网络提示、磁盘空间检查 |
iOS 上热更脚本引擎是有审核风险的,Lua 相对安全,但下发 .lua 源码/字节码仍可能触发审核问题——上线前必须确认当时的审核口径。
监控
| 指标 | 为什么 |
|---|---|
| 更新成功率 | 直接反映分发是否有问题 |
| 下载耗时分布 | CDN 与包体 |
| 更新后崩溃率 | 最关键的"坏了没有"信号 |
| 版本分布 | 决定什么时候可以下线旧补丁 |
| 回滚次数 | 兜底逻辑是否生效 |
没有监控的热更新等于盲飞。至少要有"更新后 24 小时崩溃率"这一条。
常见坑
| 坑 | 说明 |
|---|---|
| 先挂载后写版本清单 | 崩溃导致半更新状态 |
| 直接下载到目标文件名 | 中断产生半截 pak |
| 不校验就挂载 | 截断文件导致随机资源错误 |
| 补丁链过长 | 中间断一环全废 |
| pak 优先级没设高 | 更新不生效(见 Pak) |
| 更新失败就不让玩家进游戏 | 无谓的流失 |
| 没做空间检查 | 下载到一半失败 |
| 忽略更新后崩溃率 | 线上炸了才发现 |
| 把逻辑写死在 C++ | 失去热更能力 |
| 版本回滚不彻底 | 旧补丁还在持久目录里被挂载 |
许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/b26a1f88.html
更新于:2026年10月10日