微软交流社区

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 113|回复: 15

UE4客户端中的热更新

[复制链接]

2

主题

6

帖子

9

积分

新手上路

Rank: 1

积分
9
发表于 2022-9-21 13:36:25 | 显示全部楼层 |阅读模式
客户端热更新(以下简称热更新或热更),指的是游戏玩家不无需下载全新的软件安装包进行安装,而是通过下载部分新的数据包对软件进行部分问题修复、功能增删的软件更新的一系列技术合集的总称。热更不是一个单项技术,其应用范围广泛亦不限于游戏,只有当它和特定的游戏世界划分、目标平台、分发策略、游戏引擎相结合时,才会展现其在游戏领域的独特性: 一个PC平台游戏所具有的更新手段要远多于手机平台,而手机平台所具有的更新手段在大多数时候又多于主机/掌机平台;一个国际化多语言、多渠道发行的产品更新策略要比单一语言单一渠道发行的产品要复杂的多;使用UE4引擎开发游戏和使用Unity3D引擎开发游戏所需的热更新技术集合在微观层面来看无论是打包、Patch或是加载方式都会有较大的差异。
本文述及的一整套热更新系统实际上不光用在过往的几个游戏项目上,实际上早期亦脱胎于某大DAU的应用软件,升级策略和思想实际上对APP、U3D\UE游戏没什么区别。文章分第一部分之前有放出来过,这儿为保证其完整性依然放在开头。

  • 客户端软件常用的更新策略
  • 客户端软件基础更新流程和常用中间件
  • UE4游戏客户端热更基础

客户端软件常用更新策略
全新安装策略
用户需要在软件供应商更新软件之后重新获取完整的软件安装包重新在客户机上进行软件安装才能实现软件的更新。其基本流程如下图所示


全新安装的更新策略主要适用于逐份贩卖软件拷贝版权商业策略的软件,是软件商业化中最传统的软件更新策略,同时该策略也常做为其它软件商业模式的保底策略而存在于绝大多数软件中。

静默升级策略
软件自带全套升级工具集,在服务端对软件升级之后,软件客户端在后台进行软件下载和安装升级其全部或部分功能,既保证软件的最新功能可以持续到达用户又保证升级过程对用户透明,从而提高升级过程中的用户体验。其基本流程如下图所示


静默升级策略一般适用于免费的PC平台的客户端软件,手机平台上因为可执行文件的写入权限受限,静默升级在非手机厂商的软件中一般不具备完整升级的技术可行性(ROOT和越狱情形除外)。大部分手机平台中二进制可执行文件存储在只读目录中且不允许额外加载可写目录中的二进制可执行文件,一旦涉及到这些可执行文件的更新,则只能执行全新安装策略的更新。
Android平台上尚且存在可以部分绕过该权限的情形,这些绕过的情形大多情况复杂且每个Android版本、每个手机厂商定制的Rom可能都需要做额外定制,这些绕墙和定制工作繁琐艰涩,维护成本高。但动态业务发布能力对许多平台型的企业来说不可或缺,故目前国内一二线厂商都会有自己的动态发布框架,即所谓Android的组件化技术框架。需说明的是本文不会涉及到Android组件化相关技术的介绍,有进一步了解需求可自行搜索相关技术文章和开源代码库,腾讯阿里360均有不错的开源实现。

强制升级策略
软件自带全套升级工具集,在服务端对软件升级之后,软件客户端启动时弹窗提醒用户只有软件升级之后才能继续使用。强制升级过程中往往伴随着显式的软件下载和安装的等待时间,故其用户体验较静默升级较差。该策略基本流程如下图所示


强制升级策略因其升级体验不佳,对一般软件来说适用于有重大缺陷、软件兼容性或者重大功能更新的场景。但对于游戏来说,绝大部分产品的唯一升级策略就是强制升级,其主要原因是大多数游戏品类是内容消耗型的产品,而内容消耗又决定了产品本身的留存和收入水平,或者从产品逻辑上就无法做新旧内容的版本兼容所致。

灰度发布/ABTest策略
灰度发布是指在软件更新时对不同用户执行不同升级策略的升级方式。灰度发布可以做到只对一部分用户进行版本升级,也可以对不同的用户执行不同的版本升级。它的主要应用场景是在软件功能或代码有重大更新后做对比测试用于规避在用户体验、可用性或稳定性方面的风险,也可以用于疑难Bug修复的覆盖测试。

客户端软件更新流程和常用中间件

更新流程
完整的软件更新流程从研发期到真正的到达用户完成更新可简化为三大步骤,具体情形如下图所示




  • 升级Patch生成

升级Patch生成的完整步骤涉及到软件开发、测试和CI三部分的工作,下图是该步骤更详细的流程


通过“构建打包”得到新的软件版本,只有在通过QA完整的测试之后,再继续生成各版本之间的差异信息(Diffs),然后根据版本间的差异信息再生成版本间的升级补丁包(Patches),并把这些版本信息、Patches上传到对应的版本服务器进行保存,做为可发布的版本备份。
对于大多数非游戏类产品来说,会更多考虑用户体验的友好程度及考虑到版本的兼容性问题,Diffs和Patches的生成会同时生成多个版本之间的差异包,从而保证绝大部分用户在任何时间点都只需要一次升级就能得到软件的最新版本。但游戏行业在升级的通病则是如果用户的版本是0.1,最新的版本是0.5,则用户打开版本需要4次升级。


  • Patch分发预备和升级配置

Patch分发预备和升级配置的工作一般来说是运维或运营部门负责,其主要的工作包括:Patch数据上传到CDN源站、预估和申请CDN预留带宽、配置软件各版本\用户群\渠道的升级策略和升级说明、推送升级策略到达服务器/用户等。其详细流程如下图所示


这部分工作往往会被仅有软件或游戏客户端开发经验的同学所忽略,但其重要性实则不亚于更新相关的其它两部分:一是升级策略和CDN带宽配置的合理性要求高,不合理策略可能会导致软件不可用或带宽消耗过高,从而推高升级带宽的成本,或是拖长用户升级版本等待时间\拉高用户升级失败率使用户体验变差。二是升级策略往往需要配合灰度发布(ABTest)、运营策略或商业策略,有助于制订更合理的商业和产品决策。


  • 客户端更新Patch

客户端更新Patch这一步是客户端开发最常见能接触到的部分,该步骤大的流程分三步,如下图所示


一是获取最新的版本信息,检测是否有版本更新,二是在有版本更新的前提下,从CDN下载所需的更新Patch到本地,三是在Patch下载完成且需要做版本更新时应用Patch到客户端,完成客户端的版本更新。

更新中的中间件
如前文更新流程所述,软件(包含游戏)更新过程实际上涉及到研发、QA、CI支撑、运营、运维等许多职能部门,其所涉及的技术门类广泛,是一个技术集合而不是一两项技术所能蕴含。这个技术集中许多功能已有非常成熟易用的中间件供选。下面介绍一些开源免费的软件更新中常用的开源免费技术中间件。

压缩库
ZStd: 平衡运行时CPU消耗和压缩比的冠军,一般对运行时间要求不太高之情形下推荐它。ZStd在所有方面都可以替代Zlib,Zlib目前存在的理由只剩下历史包袱这一个原因了。
LZ4: 两种压缩模式,解压效率高,默认模式压缩时效率高但压缩比低(接近2.0),适用于对运行时间要求高的情形,如网络包压缩,高压缩模式压缩效率非常低(只有默认模式的5%)。
LZMA: 压缩比之王,但是运行时效率不高,适用于存储空间敏感或运行时效率过剩的场景,如PC端的软件Patch压缩。
在ZStd的官网上有关于ZStd,LZ4及Zlib等相关压缩库Benchmark,基于I9-9900k 5.0GHZ的CPU(该补上一个在手机平台的测试数据),如下所示


二进制补丁(Binary Patch)库
对于可执行文件或资源文件只有少量改动的情形下,使用二进制补丁能显著降低更新包的大小,尤其是相邻版本之间可执行文件、Lua脚本、配置文件的修改,二进制补丁可以让几十MB的更新补丁下降到几KB几十KB。
ZStd: 在1.45版本之后,ZStd正式引入了二进制补丁的生成,其生成包体的速度为传统二进制补丁库bsdiff的数倍且其生成包体还优于bsdiff。到此ZStd在此也已经有足够的理由让bsdiff也退出历史舞台。
bsdiff:差量生成使用的是sufsort算法,自带了bz2算法做压缩,Chromium开源实现中物权法sufsort算法替换为了divsufsort算法。bz2算法在压缩率和解压速度方面甚至被lzm2所碾压,其核心优势是内存占用较小。

Http/Https库
libcurl:支持Https需要Openssl ,万能的Http库,支持Cookies、用户名密码授权、重定向、代理、断点续传,也支持诸如FTP、SMTP、POP3等等其它协议。

加密解密/签名算法库
openssl/libcrypto: 实现了主流加密算法和签名算法(包括MD5)。

ASTC压缩库
astc-encoder:arm 的astc压缩库,截止2020年11月一直在持续更新和维护,完整支持各种格式的astc格式压缩,缺点是纯c++实现,压缩效率一般。
ISPCTextureCompressor: 使用ispc编写的astc压缩库,压缩速度较astc-encoder快,但缺点是支持的astc格式不完整(如不支持HDR格式的astc压缩)。

UE4游戏客户端热更

这儿讨论的热更技术在PC上可以同时更新C++所生成的可执行文件和游戏执行所依赖的资源文件,但在移动端则仅支持可执行文件外的游戏资源文件热更。即移动端的C++热更不在讨论之列。同时,明确一下游戏资源的定义,游戏资源指的是UE4游戏在打包时可以包含在PAK包体中的所有文件,如有必要,也可以包括包括运行时生成的配置文件(ini)或下载的其它数据文件。
从技术层面上来说,和引擎相关的热更实现只和上一节的软件更新流程中的第一步(升级Patch生成)和第三步(下载和应用升级Patch)有关,在上一节中讨论了这两步的基础执行流程,在这一节中我们将讨论的是流程中的每个子节点实现所涉及的问题、依赖关系和可能的解决方案。

升级Patch生成

资源分类(Resource Category)


ShaderLibrary : 在工程中开户"Shared ShaderLibrary"之后生成的Shader库文件。ShaderLibrary类似于Shader数据库,它把每个Material/MaterialInstance中的Shader收集并去重存储在一个自定义格式的文件中。使用ShaderLibrary的好处是可以极大的降低游戏包体大小(在材质实例按千计的时候,包体比不开小个2G是很正常的操作),故找不到不启用ShaderLibrary的理由。
Blueprint(蓝图)资源:虽然可执行,但它走的是虚拟机,一般蓝图可以当作普通的UAsset处理。
UI蓝图:UI蓝图在Cook时子UI会自动填充到父UI中,即在编辑器中看到的UI父子引用关系在发布时会塌陷成单级UI。
Level BuildData: 它内部包含了大量的关卡唯一数据(导航网格、ILC、IBL、光照图等),对关卡的任何修改都可能导致Level BuildData变得和原来不同。在Cook的时候部分流式加载敏感数据会被从BuildData中移出(如Lightmap)成为单独的数据。
PSO Cache : 渲染状态和Shader数据Cache数据。
Lua:  脚本文件,非性能敏感的游戏逻辑和UI响应逻辑一般都会写在脚本中。一般Lua源文件在发布时会打包成ByteCode,这样既能减少一些包体和内存占用,又能大大提高Lua的初始化和加载速度。
Configs(配置文件): 策划\美术\引擎的配置文件,一般源文件为Excel,发布文件为csv、json、lua、ini、datatable或自定义的二进制格式,csv/json的优化不到位会极大的影响游戏的初始化速度。

版本号(Version)
一般版本号的形式为A.B.C.D ,定义为4位或3位有数字。试以4位数字来示例版本号定义,每位取值为0~255,第四位版本号中前几位表达的是地区编码(如zh-cn,zh-tw,en-us,en-uk等), 末几位表示特殊编号,可用于表达Alpha、Beta等特殊版本,也可以用于表达发布的渠道号,这样版本号可以使用一个32位的无符号整数所表示,如下图所示


这样定义之后,诸如如UE4.23.1 Alpha版 就可以很方便表达出来。

差异数据(Diff)
从通常意义上来说,数据的差异有如下三种情形:


  • 有文件(或文件夹)增加
  • 有文件(或文件夹)删除
  • 有文件内容被修改

其中脚本、配置资源、ShaderLibrary、ShaderCache等资源主要的差异来源于第三种情形;显示相关的美术资源则上述三种情形都会发生。
为了生成版本间的差异数据列表,我们就需要比较当前版本和目标版本的资源数据,找出上述三种差异并导出成数据列表。伪代码表示如下


这其中的UI蓝图在修改时需要特殊处理,使用handleUMGModified函数扫描引用它的所有双亲蓝图并加入到modifiedFileList中,伪代码如下所示



生成补丁(Patch)
使用差异数据列表之后,就可以利用差异数据生成补丁文件用于客户端升级之需。补丁中包含的数据如下图所示




  • 按需打包: 生成多个更新包,如为一个角色皮肤单独打包,或是为一张新地图单独打包,此需求可通过读取打包配置实现
  • 压缩方式: 接入ZStd,替换UE4中集成的zlib方式压缩
  • 二进制补丁:对于上图标红的修改过的文件,使用二进制补丁方式生成单个文件Patch而不是把完整的文件打入升级包内,可大大提升升级速度。二进制补丁生成同样使用ZStd

生成补丁的伪代码示意如下



客户端应用Patch

升级包加载策略
在完成升级补丁生成之后,游戏客户端可以进行补丁下载和应用补丁到当前游戏中。这一步最核心的技术点有两个:


  • UE4如何挂载资源包?

UE4的Pak资源包通过FPakPlatformFile类进行挂载,和挂载相关的接口如下所示


Mount:挂载单个具名Pak资源包,是挂载资源包最基本的接口
UnMount:卸载单个具名Pak资源包
MountAllPakFiles:挂载某个目录下所有的资源包,两个重载的区别只是是否支持自定义的文件扩展名,MountAllPakFIles内部调用Mount进行真正的挂载


  • 当一个资源同时在多个资源包存在的时候(冗余升级),如何确定UE4的文件系统加载的是最新的那份资源?

UE4的Pak搜索规则是在挂载的时候由上述Mount函数的参数PakOrder所定义的,PakOrder越大,其优先级越高。
那么默认的PakOrder由哪些因素决定的呢?
(1).Pak存放目录自带优先级
UE4默认相关目录中Pak的优先级从高到低分别为:


其关代码实现在FPakPlatformFile的GetPakOrderFromPakFilePath函数中


这样定义的好处是:项目Content内的Pak可以优先加载,用于覆盖引擎中的Pak,但为什么Content的优先级要高于SavedDir目录下的Pak呢,一般升级下载回来的Pak不都是放在SavedDir目录下吗?答案是Pak的优先级不只由目录所决定。
(2).Pak命名中可以隐含版本号,版本号的优先级要远大于目录
带版本号的Pak命名规则为:
PakName_VersionNumber
最终的PakOrder为:
PakOrder = DirectoryOrder + 100 * VersionNumber
代码实现在Mount函数中,摘录如下


这样Content/Pak/Test.pak的优先级就一定会远小于SavedDir/Test_1的优先级,同时如果在SavedDir中存在多份同名不同版本的Pak,也可以透过版本号来区分其优先级。
(3). 如果你已经启用ShaderLibrary ,升级完PAK之后需要重新加载ShaderLibrary。如果你已经加载某些配置,也需要刷新配置文件以更新配置内容。
加载ShaderLibrary的接口如下所示



升级的平台相关性
各平台上的文件操作权限和目录分析如下表所示


移动平台应用所具有的文件读写权限结构和PC大不相同,在IOS/Android上带在安装包内的资源包在后续的升级过程中无法被改写,这也是为什么升级之后,会一份资源会存在多个Pak中的最主要原因——对移动平台来说,升级Pak一般存放在SD卡中。

版本更新和下载部署中的基础体验


  • 优先选择Https协议而不是Http协议:因为Http协议很容易被劫持,从而是下载链接和检测接口

失效。早年某个产品在陕甘两地的一些用户群体中更新失败率非常高,联系用户上传更新文件,最后是某些中小运营商在本地做了不正常的升级包Cache,导致用户下载的一些包体是老版本的数据。虽然新包和旧包的签名不同,但他们的Cache策略却只认http请求的URI而不检测文件本身是否有变更。


  • 断点续传:需无条件支持,用于应对用户网络抖动和断线的情形。
  • 下载包检验:下载完成之后需要对包体进行签名检验,用于确定下载的包体是否完整,一般使

用MD5或Sha1签名


  • 垃圾清理:资源包占用空间大,需要有清理规则,一般来说清理规则会有:定时清理 、 空间

占用大小、清除未能识别格式的数据等。

Patch The Pak
如果没有使用二进制补丁模式的Patch,那么直接解压Patch为Pak,并挂载到UE4的文件系统中就能完成一般资源的更新了。
如果使用了二进制补丁模式的Patch,则需要从旧版本的Pak中解压对应的原始资源,使用Patch数据对原始资源进行整合从而生成新的资源,最后把新的资源整合为新的Pak包,从而完成升级。需要注意的是应用二进制补丁的过程中可能会存在异常,也可能补丁失败,在达到重试次数之后需要有Fallback方案用于下载非二进制补丁包进行升级。
该流程使用伪代码表示如下图所示(为示例简洁性,二进制补丁失败直接回退到文件级Patch)

回复

使用道具 举报

0

主题

2

帖子

0

积分

新手上路

Rank: 1

积分
0
发表于 2022-9-21 13:36:40 | 显示全部楼层
非常好奇现在游戏行业为什么会出现“0.1版升级到0.5版需要4次升级”的情况,是因为相对来说升级包比较大么?
回复

使用道具 举报

0

主题

1

帖子

0

积分

新手上路

Rank: 1

积分
0
发表于 2022-9-21 13:37:28 | 显示全部楼层
其实你说的有一点不严谨,unity是可以给native代码打patch的,包括原神在内的游戏都在大量使用,可以去了解下InjectFix
回复

使用道具 举报

1

主题

4

帖子

5

积分

新手上路

Rank: 1

积分
5
发表于 2022-9-21 13:37:35 | 显示全部楼层
injectfix不是修复native c++代码(比如引擎底层的问题),不过确实有手段可以热更c++代码,但需要提前打桩。
回复

使用道具 举报

0

主题

1

帖子

0

积分

新手上路

Rank: 1

积分
0
发表于 2022-9-21 13:38:18 | 显示全部楼层
是啊,C#非常容易自动打桩,而且是原地修改,C++要打桩就比较麻烦
回复

使用道具 举报

0

主题

6

帖子

0

积分

新手上路

Rank: 1

积分
0
发表于 2022-9-21 13:38:49 | 显示全部楼层
太tricky就没写了,也是可以基于标记自动处理的。
回复

使用道具 举报

0

主题

3

帖子

0

积分

新手上路

Rank: 1

积分
0
发表于 2022-9-21 13:39:37 | 显示全部楼层
一般来说不是基于标记的,标记太麻烦了,无论是xlua还是injectfix,都是通过linq自动筛选然后打包时自动插桩的,逻辑代码不需要任何操作
回复

使用道具 举报

1

主题

4

帖子

4

积分

新手上路

Rank: 1

积分
4
发表于 2022-9-21 13:39:49 | 显示全部楼层
自动插桩对于大工程尤其是引擎是不适用的。代码量太大。
回复

使用道具 举报

1

主题

3

帖子

4

积分

新手上路

Rank: 1

积分
4
发表于 2022-9-21 13:40:10 | 显示全部楼层
我们测试对关键部位自动插桩总的大概有几w个桩,对性能影响没有测试出来,而且这两个插件也都建议自动插桩,而且用linq非常容易过滤目标方法,比如自己项目的代码段的namespace都有特征
回复

使用道具 举报

1

主题

3

帖子

5

积分

新手上路

Rank: 1

积分
5
发表于 2022-9-21 13:40:56 | 显示全部楼层
ZStd patch 支持非CLI场景吗
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|微软交流社区

GMT+8, 2025-1-22 12:50 , Processed in 0.089558 second(s), 18 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表