UUG北京站 | 团结引擎的H5项目优化实践分享
今天分享一些研发当中的优化细节和最佳实践。先介绍一下我们的游戏《Dreamland(DL)》,它是 SLG 题材游戏,它的主城家园有类《饥荒》的外围包装,后面再引导进入大世界地图。先介绍一下主要场景,游戏当时立项面向手游,目标是在 Native APP 里面榨干手机所有的性能,还引入了光影系统。虽然是 2D 游戏,但规模是比较大的,从下方视频可看出算是一个重 CPU 规格的手游。
演讲资料PPT下载地址:https://u3d.sharepoint.cn/:b:/s/UnityChinaResources/Ec9zJOxlCBRCsjBcQ9mV84MB5-sssXL2_BGVAf7lNPvi8w?e=vSEPgt
在 2025 年 5 月 24 日的 Unity User Group 北京站活动中,壳木游戏的 CTO 邝圣凯带来《团结引擎的 H5 项目优化实践分享》。本文为演讲全文实录。
观看演讲视频:https://www.bilibili.com/video/BV1JWjRzVEHR?spm_id_from=333.788.videopod.sections&vd_source=6ad5666ecbc7fe0e80d963da7e237d92
感谢大家,我们作为团结引擎的用户方,今天也很荣幸得到 UUG 的邀请,介绍我们通过团结引擎做一个手游项目时向 H5 迁移实践方面的经验。
游戏介绍
今天分享一些研发当中的优化细节和最佳实践。先介绍一下我们的游戏《Dreamland(DL)》,它是 SLG 题材游戏,它的主城家园有类《饥荒》的外围包装,后面再引导进入大世界地图。
先介绍一下主要场景,游戏当时立项面向手游,目标是在 Native APP 里面榨干手机所有的性能,还引入了光影系统。虽然是 2D 游戏,但规模是比较大的,从下方视频可看出算是一个重 CPU 规格的手游。
allowfullscreen="true" border="0" frameborder="no" framespacing="0" scrolling="no" src="https://live.csdn.net/v/embed/482111">
我们的大地图战斗规模还是比较大的,支持英雄的种类、Spine 体量比较大,战斗达到 200vs200 同屏最高规格,英雄 Spine 50vs50,每个英雄带 3 个小兵 UV 动画 150vs150。
下方视频中展示了比较极端的战斗场景。我们当时决定把这个游戏迁移到 H5 版本时也做了比较久的权衡,进行了不少可行性分析。
allowfullscreen="true" border="0" frameborder="no" framespacing="0" scrolling="no" src="https://live.csdn.net/v/embed/482110">
挑战和痛点
总体痛点如下:
第一是进游时间,对于 Native APP 用户可以到 App Store 下载,然后进行游戏,游戏大部分前期资源都是已经准备完成的,不存在 H5 进游的时候零占用开始准备加载的过程。
第二个突出问题是内存问题。这个游戏面向 Native APP 设计,内存是比较超标的,到 H5 方面,比如做微信小游戏,可用的空间只有 1 点几 G。这个游戏在 Native 版内存峰值可以达到 1.8G 接近 2G,在一些比较老的 iOS 设备上就会崩掉。
第三是功能响应速度,H5 算力并不差,但有些资源是后置加载,一些前期功能用户在用到的时候可能资源还没有下载完,如果安排不好就有加载响应的问题。
最后是帧率问题。H5 虽然 GPU 没有太多折损,但 CPU 能力大概是 Native APP 的三分之一,包括多线程不能被支持,这是比较不利的地方。
介绍一下宏观思路。解决内存问题一个比较重要的是通过微信代码的分包机制,再就是开启团结引擎设计的高性能+的模式,以及 Spine 优化、游戏业务相关的优化,后面会详细介绍。
OOM 问题解决之后,运行期方面我们主要重点是在 Spine 性能优化上。包括 Spine 拆面主要解决内存问题,运行时的效率问题也能够得益。我们根据机型限制 Spine 动画的 Update 频次,根据兵种大小降到 15fps、20fps,在有些比较小的角色上是看不出来的,对 CPU 是很大的节省。还有 2D 管线优化。预加载预实例会用到异步的实例化。再就是特效裁剪、相机 SkipCulling 等等。
为什么选择使用团结引擎?我们立项的时候做 Native APP 使用的是 Unity。转团结引擎是因为团结专门对 H5 做了大量的优化,包括刚才说的进游时间、内存优化、WebAssembly 内存峰值等当时使用 Unity 不可逾越的问题。我们等了大概半年时间,团结引擎 1.0 release 之后马上跟进,确实能够达到很好的效果。如果大家计划做 H5,建议使用团结引擎。
下面分几个方面展开:资源、管线、启动、运行时优化、上线前部署。
资源管理
资源管理,分几个角度展开:
首先是颗粒度的问题。APP 版打 Bundle 是好几个资源放在一起,但 H5 版资源是按需下载,用到 A 的时候才下载 A,前期没有办法把所有的资源都下下来。如果下载的 A 资源处在比较大的 Bundle 中,会占用比较长的下载时间。所以我们把资源切到一个非常细粒度的层次,确保每个 Bundle 非常小,缺点是要下载 Bundle 的数量更多。不过 HTML 连接是复用的,实践中比合并打 Bundle 的策略要好,做 H5 项目的时候可以考虑尝试,Bundle 的粒度尽量控制得比较小。
第二,我们做微信小游戏用的是 WXAssetBundle,相对于 Unity Bundle 会做一个基于磁盘的局部检索,加载里面某一个资源的时候只会读取中间某一段资源的数据,而不是把整个 Bundle 放到内存里面去做读取,对内存的占用比较有好处,也是微信小游戏、抖音小游戏官方推荐的机制。
资源压缩也是老生常谈了,关闭 Read/Write, Generator Mipmaps 选项,使用特定的压缩规格。
具体的资源管理使用的是 AssetGraph 对每个资源进行压缩规格的管理。
使用 SpriteAtlas 变体。因为我们一边在开发 Native APP,一边做 H5 版本,两条线并行,所以 H5 方面我们对某些图集做一些 Scale 压缩,通过修改 AtlasVariant Scale 来实现 H5 的更加轻量化,同时保持跟 Native APP 相对稳定的链接。
再就是 Secondary Texture,有些 sprite 设置或使用了一些第二贴图,但第二贴图有时候是不必要的,这也是一个检查点。
资源下载方面,之前已经讲过推荐使用 WXAssetBundle。
卸载的时候需要特别注意,之前的 Bundle 在卸载时一般会基于引擎做一些辅助管理,做一些半自动的回收。比如保守的策略,在资源有可能在被占用的时候引擎就不会真正的卸载。但是到 H5 这样对内存要求比较严苛的平台,我们建议完全强制管理资源卸载,使用强制卸载的选项。缺点是某一处引用可能还在,没有注意到,可能卸载之后游戏产生错误。建议让这个阶段在测试期尽可能暴露出来,并且修复,以此来换取比较干净的或者更加受控的内存管理,这是策略上的选择。
URP管线定制
接下来讲 URP 管线定制。我们的游戏虽然是 2D 游戏,但对光影方面做了相当大的工作,有自己的光照系统。中间有一些流程管线的简化都是共通的,比如移除 XR 相关、删除不必要的 Graph 组件、3D 光影系统等。
接下来是我们针对这个游戏光影系统做的优化。SortingLayer 我们用得比较少,对游戏排序跟光影控制有我们自己的算法去实现,单独做光影烘焙,包括对象排序都是自己实现的,所以光照相关的 SortingLayer 我们取消掉了。
混合模式管线里面带了 4 种,我们都去掉了,只留下简单的 Multiply,辅以美术对贴图的控制处理来做集中方式的表现。视锥体的 culling 我们基于游戏本身的逻辑自己去做,对复杂的光照和视锥 cull 选择性取消,也是一个可能的优化点。所有光源烘焙成小的光照图,相当于缓存机制,实时布灯在场景里面,但布灯操作不是特别频繁,通过 DrawMeshInstanced 把它渲染出来。
启动速度 - 首包优化
接下来进入到首包优化方面的介绍。首包很影响游戏速度,分为资源包和代码包两部分。
通过 AssetStudio GUI 工具分析首包场景,可以发现一些明显体积比较大的包体资源,包括一些不合理的引用进包,这是最容易忽视的。
进一步剔除掉一些没有用的 Package。DL 剔除了 analytics、unityanalytics、recoder、android-logcat、uielement、androidjni、physics2d、terrain、unitywebrequestwww、vehicles、wind、remote-config、test-framework、xr、terrainphysics、remote-config、jobs、TeshMeshPro、Burst、Mathematics 也是不需要的。
接下来介绍团结引擎专有的 Wasm Inline Threshold。Inline 即内联函数,把一些函数实现注入到调用的函数里面去,会增大代码体积,但能够显著运行时期的性能,少了入栈出栈的操作。团结引擎这个选项我们目前会做一个控制,适当设置 Threshold 阈值,对运行期有帮助,这是一个可以参考的优化点。
代码编译选项设置,除非资源特别大,一般选择 Faster Runtime。
然后是代码的 Strip,团结引擎针对引擎代码提供 Strip Advice 建议选项,对于一些不用的模块可以勾上。
有的时候 Strip 比较多的时候团结引擎提供 DryRun 方式,可以在运行时把 strip 掉的并且仍然在调用的函数打印出来。
Lua 方面,我们使用了 Lua & xLua,也做了一些相关的 Lua Binding 的优化,对一些不需要用 API 的 Binding 通过黑名单去掉,精简一部分代码体积。
最后是 Wasm Analysis 工具,可以统计每个函数的指令数量及影响程度。对于一些不寻常的系统调用,或者一些间接的调用,可以通过这个工具发现有没有剔除到位。
分包是微信开发者工具里面提供的代码分包机制,把加载期的代码跟后期使用的代码或者不需要用的代码分割或者剔除,这是非常重要的一个减少首包大小的方式。而且减少游戏加载之后的 WASM 编译时间。
分包涉及到调用函数的收集,理想的情况下我们会把不需要调用的 API 或函数去掉,如果有新的函数调用增加进来,通过这个收集捕捉到,追加到名单里。它提供的 Profile 分析包就是用来收集的,在游戏中统计哪些函数可以调用到就把它保留下来,如果哪些函数没有调用到就去除掉。有的时候一开始去的比较激进,调用失败的时候会把日志打印出来,最终补充进去,最后通过 release 的方式生成和发布。
启动速度 - 启动优化
启动优化,是进入游戏之前先代码下载、代码编译、资源下载,再进入启动画面的阶段。
启动画面出现之前的预下载阶段比较重要,微信会在这个阶段提供比较多的下载队列,可以同时进行多个队列下载。因为默认这个阶段 CPU 渲染占用比较低,比较重要的资源建议放在这里下载。
下载的配置建议不要用固定文件名,而是要用文件名的 MD5 或文件名带版本号,解决下载中缓存的问题导致更新失败。
鸿蒙预下载机制目前有一些问题,后期可能会修复。
启动速度优化的一些策略,首先第一个场景尽量小,最好只要一个背景图,尽快让用户能够看到你的游戏画面。Shader 是很大的加载负担,所以 Include Shaders 尽量能删就删,Preloaded Shaders 尽可能清理干净,否则会显著增加第一场景的进场时间。
关于 Shader 变体 warmup,Unity 默认提供了 Shader 变体 Warmup 机制,我们建议采用 WarmUpProgressively,或后面会推出的 WarmUpAsync 异步 Warmup 机制。因为 Unity 默认的 Warmup 是阻塞的方式,会导致 H5 在加载时卡很久。当时我们做的时候还是内部接口,后面应该会放开。通过异步的方式预热 shader,避免游戏阻塞。
运行时优化
运行时优化分为以下几个方面:
下载方面,可以考虑做一个任务池。后台下载对 H5 非常重要,可以把一些高优先级的任务、必须立即下载的任务、非高优先级的任务等做队列的管理。
文件 IO,跟 Warmup 一样非必要不同步。H5 里面能不阻塞的调用尽量不要阻塞,包括 IO 一键读取也会容易掉帧,推荐采用异步存取的方式。
字体方面,为了提高加载速度我们做了几个层次的优化。刚开始进入游戏使用 2M 的只包含游戏内部使用的字体;后期需要加载较完整的字体,因为涉及到用户姓名和聊天的显示,会使用微信字体。
但微信字体在 iOS 18 后目前有一个 BUG 暂时不能使用,未来会修复。在这之前我们会提供一个全字体 fallback 机制,在加载失败之后会 fallback 到全字体,所以目前是有三套。
再就是一些针对机型分辨率降低的适配方式。微信提供了 API,能够获取机器的等级。Benchmarklevel 是一个绝对的值,目前是不超过 50,它不会随着时间推移改变,机器是多少级永远是多少级。
ModelLevel 表示相对于当前年代,机型属于当前的高端机、中端机、或低端机。它会随着时间变化,比如今年的高端机型,过了五年变成中端或者低端。大家根据具体的范畴使用这两个参数做相应的适配。
音频方面也比较简单,注意不要同时播放太多音效,尽量能够自己去管理。我们除了背景音乐以外音效控制在 3 个以下,如果特别多会关闭一些音效。
Spine 是我们这边 CPU 最大的瓶颈。首先是资产优化,再就是一些规格的优化。骨骼数同屏不超过 3000,单个 Spine 骨骼数不超过 70。但是我们角色比较复杂,为了让骨骼数降低,我们对角色朝向进行了拆面。本来每个英雄所有朝向都在一个 Spine 里面,现在我们把各个朝向的角色拆成了不同的 Spine,可以节省运行期的内存和性能。
对一些比较小的个体不使用 Spine,我们把 Spine 烘焙成 GPU 动画的方式,把 Spine 每一帧顶点信息存储在贴图里,然后在 GPU 里面根据这个时间去插值每个顶点的位置,把顶点绘制出来。这个方式有一个局限,必须要求 Spine 是纯顶点动画,如果 Spine 包含顶点之外的动画变化就不能用这个方法。这个方法对性能有比较大的提升,特别是对屏幕数量特别多的小兵,优化还是非常大的。
再就是 Spine 读取,使用 NativeArray 代替 TextAsst.byte,提升性能。
延迟加载是把英雄的动作进行拆分,每次按需加载一部分动作,而不是把所有动作都加载进去,可以减少内存的占用。我们本身项目角色动作会占用比较大的内存,多达 200M。
再就是视口裁剪等常规的手段,超过视口的 disable 掉,不让它更新和计算。
其他优化,首先包括 UI 优化。整屏 UI 弹出来以后让背景场景的渲染暂停。
第二,相机的 Culling 优化。团结引擎提供了 Skip Culling 功能,我们会做一些自己内部的 Culling,把 Culling 放在选项里面去,在需要自己 Culling 的时候就跳过相机 Culling,减少相应的开销。
还包括做一些减法,比如 Native 中一些植被的动画,我们在 H5 里面把它变成静帧。
还包括 UI 方面做了一些比较大的精简、特效删减等。
Texture2DArray 无法正常支持,所以我们做光影烘焙的时候用的是 Texture 方式代替 Texture2DArray。
上线前部署
最后讲一下上线部署。如果大家考虑要热更的话,因为资源是通过 URL 去标识的,同一个 URL 会有缓存的问题,如果 URL 不变,文件内容变了,有可能会缓存到旧的,所以需要注意把版本号资源号放到文件名里,或者下载目录包含版本号的信息,以作区分。
最后是 CDN 预热。大家部署的时候可以在服务器这边做 CDN 预热机制,也会提高前期下载效率。
因为我们是一个比较新的项目,在开发过程中跟团结引擎团队、微信团队有密切的合作,谢谢大家。
更多推荐
所有评论(0)