技术博客

资源热更新:导出后替换 PCK 的可行性边界

第 104 / 149 篇 · 2025-05-28 · godot · 资源管线工程实践发布

手游玩家都懂热更新:应用商店版本是三个月前的,打开游戏先下载 500 MB 补丁再进新版本。Godot 在这件事上给了基础设施——运行时挂载 PCK 补丁包,但只给了一半,另一半要自己造。这篇先把引擎给的那半讲清楚,再划边界。

引擎给的一半:运行时挂包

PCK 文件的寻址结构决定了它可以追加挂载。一个 PCK 的目录表在文件里自包含(PCK 格式那篇细讲),可执行文件启动时从自己尾部找主包的目录表;找到之后,ProjectSettings.load_resource_pack() 可以把另一个 PCK 挂进资源查找链:

# 下载完成后
var err := ProjectSettings.load_resource_pack("user://patch_v12.pck")
if err != OK:
    push_error("补丁挂载失败")
else:
    get_tree().change_scene_to_file("res://main.tscn")  # 重进主场景

挂载语义一句话:后挂的包优先。查找 res://foo/hero.png 时从最后挂载的包开始往回找,补丁包里有这个路径就用补丁的,没有就落到主包。同名即覆盖,没有"部分文件"概念——补丁包是路径级的全量替换。

多包挂载还支持更细的分法:把游戏拆成 core.pck(引擎自带)+ content_a.pck + content_b.pck,按 DLC 粒度挂卸。load_resource_pack 还有第二个参数控制是否"只新增不覆盖"(replace_files = false 时包内已有路径不被替换),适合做纯增量 DLC。

让热更不生效的两个坑

挂上补丁包不等于游戏自动变新,两个缓存层会拦住你。

ResourceCache 没刷新。 补丁挂载前 load() 过的资源还躺在全局缓存里(Resource 那篇的表),路径相同直接返回旧对象。表现就是:补丁打了,资源看着没换。处理办法分两种:把热更涉及的已加载资源全部释放(ResourceLoader.load 的 cache_mode 参数、或逐个 free)再触发重载;或者干脆补丁装完直接重启进程——商用项目普遍选后者,干净且无遗漏,补丁下载界面顺便变成"重启倒计时"。

全局类缓存不认新脚本。 补丁包里带 class_name 的新脚本不会进 global_script_class_cache(导出时生成、启动时载入的静态表),场景文件里按类名静态引用的节点会解析失败。热更脚本的安全姿势是:补丁里不引入新 class_name,新逻辑用 load("res://patch/new_thing.gd").new() 动态取,把全局注册表当成冻结的。

能热更什么,不能热更什么

能对补丁包敞开大门的:脚本(GDScript 字节码)、场景、贴图、音频、配置——所有 PCK 里的数据资源。

进不了补丁的:

  • GDExtension / GDNative 的原生库。.dll / .so 在进程启动时就加载映射了,运行时无法替换。要更新原生代码只能整包升级。这也让"把性能热点下沉到 GDExtension"的决定多了一个热更维度的代价(性能优化那篇的账要加上这项)。
  • C# 程序集。C# 的代码编译进 assembly,随包发布,不进 PCK 热更链。.NET 项目的"热更"实际做法是下载新程序集 + 重启,或者干脆放弃热更。
  • 引擎二进制本身。导出模板里没编译进去的模块(SCons 那篇的裁剪选项)不可能靠补丁长出来。

引擎不管的那半边

load_resource_pack 之后的一切都是你的活,商用方案里要自己造的部分:

版本与差量。 补丁包是路径级全量覆盖,引擎不提供二进制差量。下载体积要自己优化——常见做法是把内容按目录拆成多个 pck,热更粒度做到"这个关卡包整体换新",而不是"整个游戏一个包"。差量下载(bsdiff 之类)得自己做,引擎不拦但也不帮。

回滚与安全。 补丁挂了打不开游戏怎么办?挂载前先校验(包内带版本号和文件清单,自己验),保留上一版补丁,启动失败回退。引擎层没有事务,失败状态全靠应用层兜底。

平台政策。 iOS 不允许下载执行原生机器码;GDScript 是解释执行的字节码,不触 JIT 红线,技术上可行——但审核对"可下载代码"的态度随时间变,发布前看清当期政策。Android 侧 Google Play 的目标 API 要求对动态加载代码有声明义务。这些属于合规那篇的地盘,这里只标记边界。

结论:资源热更在 Godot 里是"可行且够用"——PCK 挂载机制可靠,坑集中在两处缓存。但它是文件层的原语,版本管理、差量、回滚、灰度,整套发行工程要自己在应用层搭。

补丁包挂载与查找顺序 CDN 下载 patch_v12.pck 校验后 load_resource_pack() 挂进资源查找链尾部 查找 res://a.png patch_v12.pck(后挂优先) ↓ 没有该路径? 主 PCK(随 exe) ↓ 还没有? 报文件不存在 挂载后必须处理:ResourceCache 已 load() 的旧资源还在缓存 → 手动释放重载,或重启进程(商用普遍选重启) 脚本热更限制 补丁不带新 class_name(全局类缓存是冻结的);GDExtension/C# 不可热更 引擎不管的半边 版本管理 / 差量下载 / 回滚 / 灰度 / 平台合规——全部在应用层自建