在游戏开发过程中,热更新是非常重要的能力,它能够让我们在不换包的情况下更新功能、修复BUG。但是哪些东西可以热更,哪些不能热更,哪些存在风险、目前没有很详细地文章进行介绍。
本篇文章我会介绍我在开发HotPatcher及项目热更实践过程中,对热更能力和热更安全的一些思考。确保项目上线时,热更的能力和稳定性是有保障的,并且在热更迭代阶段,也能够清晰地知道哪些是可以热更的,这样也能很好地评估更新风险。
以及通过周边能力建设,提前进行风险排查,把热更新流程变成自动化的,只需要版本PM来控制补丁构建和发布时机,完全不需要程序参与到热更流程。
热更流程
补丁构建
基于HotPatcher的热更打包机制:追踪原始工程变动,自动分析所有需要打包的资源,并与上个版本生成差异补丁。
补丁生成后会上传到CDN上,进行后续的测试和发布流程。
基于这套方案的热更构建流程,热更补丁的启动构建、发布均由版本PM控制,无需程序参与,中间流程是全自动化的。
运行时
运行时需要实现补丁的动态下载流程,最好在下载完毕后再统一执行挂载:
热更发布的补丁的优先级是递增的:
- 补丁的优先级比安装包内资源及全量下载资源更高
- 最新的补丁(x.3)比次新的补丁(x.2)优先级更高,确保最新的补丁能够覆盖旧资源
这样就能确保最新的资源能覆盖旧的,实现更新的目标。
更新能力
可更新内容
资源
游戏内的绝大部分资源都是可以热更的(无法更新的内容在后面有介绍),对资产的主动修改都能够正常地打包,包含修改材质影响到的shader都能够增量追踪到。
并且HotPatcher能够分析引起的被动变更的资源,也同样会打包。
下面的这些情况,就属于被动变更(HotPatcher会自动追踪):
- 父蓝图修改,影响到子蓝图
- UMG基础控件修改,影响到所有以Instanced方式引用它的控件
- 母材质的修改,影响到它的所有材质实例
- ….
HotPatcher会根据主动变更的资产,分析它们引起的所有被动变更列表,还会检查被动变更的资源存在于基础版本中(避免带入不必要的新增资源,只影响变化的部分)。
另外,还需要尽可能地减少热更完成前加载的资源(如引擎依赖的关键资源、热更界面的资源等),这些无法热更,需要尽可能减少。
为了方便排查和清理非必要的资产,我在HotPatcher中提供了辅助函数,可以在启动时记录所有热更前加载的资产,这样就可以精确地知道哪些资源无法热更,也能够在后续的前置检查中把它们给暴露出来。
文件
游戏中需要用到的绝大部分非资产文件都是可以热更的:
- 脚本代码(lua/ts/py etc.)
- db
- pb
- 多语言(game.locres)
- 语音(wwise/wem)
- PSO
- 其他自定义的非资产文件
只要它们能被打包追踪到,并且在热更完成之后才加载(或可重载),都是可以热更的。如本地化等还需要对引擎做一些改造延迟到热更后加载,这个需要根据业务情况自己评估。
ini
我之前的一篇文章有过详细介绍:UE 热更新:Config 的重载与应用
简单地说,通常对Ini的更新支持这三种配置情况:
ConsoleVariables
1
2[ConsoleVariables]
launcher.precompile.pso=1Object Config
1
2
3[/Script/UdpMessaging.UdpMessagingSettings]
EnableTransport=True
bAutoRepair=TrueDevicesProfiles
1
2
3
4
5
6
7
8
9[iPhoneXXX DeviceProfile]
.CVars=sg.ViewDistanceQuality=2
.CVars=sg.AntiAliasingQuality=2
.CVars=sg.ShadowQuality=2
.CVars=sg.PostProcessQuality=2
.CVars=sg.TextureQuality=2
.CVars=sg.EffectsQuality=2
.CVars=sg.FoliageQuality=2
.CVars=sg.ResolutionQuality=100.0
注:IOS无法动态修改
r.MobileContentScaleFactor
。Android会出现点击偏移。所以如果想要动态修改DeviceProfiles,不能修改MobileContentScaleFactor
的值,需要自己处理延迟到下次启动。
除此之外,在引擎启动时加载的ini配置,时机太早无法通过热更下发,需要通过重新出包进行修改(虽然把补丁放到自动挂载目录也可以,但这是一个危险行为,不建议这么做,后面热更安全部分会具体介绍)。
建议:线上版本的ini中的DeviceProfiles中的配置,非必要不改。如必须要修改,需在发热更时实际修改的内容具体评估能否热更以及风险。
PSO
虽然UE提供了在打包时就把PSO打进安装包的机制,但是我们并未采用,因为限定构建安装包之前采集PSO的流程我觉得不够灵活。
我们目前的PSO流程,是与构建安装包无关的:
- 构建安装包时,没有任何PSO相关的东西参与
- 安装包打出后,自动化采集PSO
- 采集完毕,自动化生成
stable.upipelinecache
并可以作为热更发布
这样就把PSO作为一个独立的流程拆分出来了,并不是一个前置要求了。而且也能够实现直接实现PSO的热更新(后续有时间写一篇PSO热更的内容)。可以在发布了几个热更版本之后重新采集PSO,并热更发布。
删除文件
绝大部分情况下,我们只需要关注新增和修改的部分。但是有人会问,如果我把资产或文件删除了,能够通过热更下发到这个删除的行为吗?
可以的!但是对于UFS层面的删除而言,要实现的并不是真实地把文件从用户磁盘上物理消除。因为文件是被打包到PAK中的,如果想要从PAK完全删除,需要重新生成PAK,这在本地执行有些风险。
而是通过让UFS找不到真实的文件,从而起到“删除”的作用。
分析引擎的代码能够知道,可以通过UnrealPak的response file文件,传递-delete
参数:
1 | "../../../Blank425/Blank425.uproject" "../../../Blank425/Blank425.uproject" -delete |
将它打包为Pak时,其实是添加了一个空的Entry,并且被标记为了Deleted:
通过UnrealPak的-list查看,它也像普通的文件一样,不过比较特殊,没有大小、offset为0,SHA1也为0:
在运行时从UFS已挂载的PAK里查找文件时,如果标记了delete的Pak优先级比较高,就会触发FoundDeleted:
还可以添加多个删除标记:
1 | "../../../Blank245/Content/StarterContent/Maps/StarterMap.umap" "../../../Blank245/Content/StarterContent/Maps/StarterMap.umap" -delete |
删除的文件列表同样会参与MountPoint的计算:
其实,如果想要标记一个资源被删除,真实的物理文件都不需要存在,因为它不会被真的打包到pak里去。只是通过这个MountPath的路径,创建出一个空的PakEntry。
让UFS查找文件时优先找到这个空的PakEntry,加载不到真实的资源,就是我们要实现的“删除”需求了。
文件标记删除,PAK的创建过程:
- 从PakList中读取所有行
- 检查是否有-delete的token
- 若有,则把Entrie标记为
bIsDeleted
- 在
CollectFilesToAdd
中,对于标记bIsDeleted
的文件,会直接跳过,不会去查找实际的磁盘文件 - 在
CreatePakFile
中,会检查FilesToAdd
的每个元素的bIsDeleted
,并会给对应的PakEntry添加Flag_Deleted
的标记,用于标识被删除。并且不会把该元素指向的本地文件给打包到Pak内(这也是随便指定一个路径也可以标记为删除的原因)
这样就实现了从paklist.txt
-> UnrealPak -> Deleted.pak -> 运行时加载PakEntry这样的一套流程了。
HotPatcher已经自动化了这一过程,对删除的资源和文件,如果Patch的配置中,使用了bIgnoreDeletedFiles=false
,则会在打出的补丁中,将它们标记删除。
这样就能够实现通过热更下发“删除文件”的行为了。
无法热更的内容
无法热更的内容,是指无法通过在游戏内动态下载更新给玩家,只能重新打完整包发布。
无法热更新的部分:
- 原生/C++代码(影响Runtime的代码,可转发到VM执行的不在此列)
- 第三方库
- 包的签名、Entitlements
- 包内的文件(如GCloudVoice的模型文件)
- Android的Manifest、IOS的PLIST等
不建议热更新的部分:
- uproject、uplugin
- ush/usf/global shader
- WorldGridMaterial等基础材质的修改(如
/Engine/EngineMaterils
,pak的挂载与ShaderLibrary加载时机不同可能会导致crash) - 热更完成前,已加载的资源(引擎依赖的关键资源、热更界面的资源等,这些无法热更,需要尽可能减少,保持依赖最小化)
除了完全无法热更新的之外,剩下的这些非要更新也不是不可以,只需要下载后放入自动挂载目录,设定一个比安装包内资产更高的优先级,然后重启生效。
因为UFS本身的机制,就是去加载当前时机下优先级最高的PAK中的文件,所以只要我们能保证引擎一启动,补丁的优先级就最高,那么UFS内的所有文件都是可以生效的。
但为了安全考虑,我依然把他们列入无法热更的列表中(机制可以提供这样的能力,但不能轻易使用)。因为商业化产品的稳定与安全性是更重要的因素,我们需要在更新能力和安全做一些取舍,下一小结会具体介绍。
热更安全
因为热更新非常重要,它是进入游戏的入口,也是版本迭代的基石,所以如何让热更本身具有比较强的健壮性是非常重要的。
有些项目可能踩过类似的坑,一个热更补丁下发下去,导致游戏无法正常启动了,只能重新换包。所以我把热更安全提到了一个非常重要的高度,哪怕牺牲一些更新能力也在所不惜。
根据我的实践经验,对于热更安全的关注应该包含下面几个方面:
- 更新的内容应该是稳定无害的
- 更新的内容是可以撤销的
- 更新的内容不会影响到下次程序启动
- 更新的内容不会影响到其他热更能力
所以,为了做到这一点,我做了下面的这些工作:
- 完全不采用自动挂载机制,所有的补丁都由业务逻辑控制挂载,完全杜绝自动挂载行为。因为自动挂载时机太早了,如果出现问题,就是致命的。
- 在1的基础上,拒绝强依赖重启生效的行为,所有的更新内容都必须实时生效(可根据项目情况评估,如对
r.MobileContentScaleFactor
等非资产属性的处理) - 统一PAK与ShaderLibrary的挂载/加载顺序以及加载时机,避免Shader和Pak内资产对不上的情况
- 完全按照优先级来控制优先级
- 每次初始启动游戏,直至热更完成,都跟新安装的情况一致,不做特殊逻辑。这样能够确保不管我们更新下去任何内容,都能正常走到热更流程,这样如果补丁有问题还有补救的空间。
除了热更本身之外,资源管理也需要严格把控:
- 确保每一次热更都是可以精确追溯的,精确记录每一个变更的资源、文件的信息,以及它们的cooked的信息,以及记录当前版本的仓库信息、生成的补丁信息。
- 检查每一次热更的资产规范,避免资产修改引起的异常带入现网。
对于资源管理而言,我们在每一次热更补丁构建之后,都充分记录了当前版本的信息:
以及归档了补丁和Metadatas:
这样可以随时基于这些数据去回溯每个热更版本的变动情况,补丁生成后也可以进行检查是否符合预期。
另外,对于参与热更的资产,我们会对所有参与进包的进行资产检查,如果有触发规则,则会同步归档并群提醒:
这样就能够清晰地知道当前补丁内有哪些潜在风险了,方便评估补丁的负面影响。
关于资源检查的部分我之前也写了好几篇内容了,感兴趣的话,可以参考资源管理分类里的几篇文章。
结语
博客内的相关文章:热更新系列、资源管理系列,本文提及的一些内容,会在这些文章中有更详细的介绍,感兴趣的可以前往查看。
本篇文章介绍了我在UE热更新的更新能力和更新安全方面的一些思考,根据实际项目上线的情况热更机制是零故障的,可以作为一种实践参考。