在 2024 年 7 月 25 日的 Unite Shanghai 2024 团结引擎专场演讲中, Unity 中国引擎底层架构技术主管兼小游戏技术负责人赵亮、Unity 中国技术经理傅有成带来演讲《团结引擎小游戏开发指南》,介绍了如何使用团结引擎高效地开发、加载更迅速、内容更丰富、画面更精致、运行更流畅的小游戏。本文为演讲内容全文实录。
最后,向大家介绍一下我们目前正在开发的功能以及未来的 roadmap。
游戏案例
《诛仙手游》小游戏是从手游版本移植过来的,上图左侧展示的是 App 效果,还有小游戏的录屏对比。可以看到《诛仙手游》小游戏版本的画质与 App 相比差别已经很小。《诛仙手游》小游戏版本在 iPhone 8 Plus 可以稳定在 30 帧,连续打两个小时副本也不会 oom。
下图是《诛仙手游》小游戏在主城里进行的多角色压力测试的结果。从数据可以看出,团结在内存和 FPS 上都有了显著提升。
下面是另一款游戏的测试数据,从 Unity 2021 升级到团结引擎后,在帧率保持不变的前提下:平均内存下降了 222MB,峰值内存下降了 293MB,游戏卡顿率下降了 11%。
下面请傅有成为大家介绍,开发小游戏各个阶段需要关注的技术点。
平台适配
2023 年 6 月项目启动,7 月在 Chrome 浏览器上跑起来,9 月在微信小游戏上跑起来,2024 年 1 月优化到 iPhone 8P、晓龙 765 能稳定 24 帧以上。诛仙的时间线其实不快,因为它是一款重度 MMO,也是项目组第一次接触微信小游戏,而且也是相对早期在微信上发布重度 MMO,踩的坑偏多。现在 1 年多过去,随着大量开发者摸索实践,还有引擎的优化,再做微信小游戏移植,效率会高很多。
接下来具体看一下平台适配需要的工作和注意点。
首先需要注意,小游戏平台目前有一些能力限制:
1. 暂时不支持多线程;
2. 不支持 socket,可以替换为 WebSocket 或者微信小游戏提供的 API;
3. 目前微信小游戏支持 WebGL1 和 WebGL2,compute shader 是不支持的,如果游戏里用到需要修改;
4. H5 沙盒环境不支持常规的文件访问能力
(1)需要使用 UnityWebRequestAssetBundle 来加载 AB;
更详细的信息可以参见团结或者微信的文档。
启动加载
1. 代码下载和编译加载,首包资源下载;
2. 引擎初始化、首场景加载、首帧绘制;
再具体看一下 callmain,下图是 callmain 的耗时细分:
1. C# 代码调用
(1)简化首帧的 awake、start;
(2)也不要在其中有一些阻塞操作;
(3)简化 C# 构造函数。
2. shader 方面
(1)不要用 Auto Graphics API;
(2)少用 always include shader;
(3)减少 shader 变体数量。
3. 纹理和音频方面
(1)使用合适的压缩格式,避免解压,减少带宽占用。
引擎这侧,我们也做了很多优化,包括代码、资源、元数据的精简,很多的延迟、按需加载等等。其中很多对内存、性能也有优化效果,后面会再提到。
还有 IL2CPP 元数据。元数据是 C# 的类型、方法信息,IL2CPP 运行时需要依赖它。默认的元数据结构可以支持 21 亿个类型或方法,这是出于通用性的角度,但对小游戏来说,通常也就是万的级别。所以我们会在打包小游戏时,根据方法数量自动选择合适的元数据结构,这样可以让 global-metadata 缩减约 15%。
边下边玩
在边下边玩方面。目前常用的方法是 AssetBundle 和 AddressableAssets,这里需要注意几点:
1. 拆分 AB 时候粒度很重要,要选用合适的大小、合理的依赖关系,这里有一个实际游戏的例子;
2. 然后要注意加载时机,做好分帧,避免集中;
3. 还要很仔细地管理 AB 的生命周期;
4. 大型游戏不建议用 Addressable,因为资源索引文件可能会很大。
团结新增了两种辅助工具,AutoStreaming 和 TextureManager,对于使用了 AB 或者 AA 的工程,可以帮助控制 AB 粒度、更好地处理依赖。没有用 AB 的工程,也可以直接使用它们来完成资源按需加载的改造。
AutoStreaming 就是自动流式加载。在 Editor 里提供了工具,打包时自动分离重度资源,包括 Texture、Mesh、Audio、Animation、Font,部署到云上,首包和 AB 就会大大减小。游戏运行时,引擎会按需从云上下载资源然后加载,开发者不用修改游戏逻辑,都是引擎自动完成的。所以 AutoStreaming 的特点就是,游戏工程的改动比较少,可以快速在小游戏平台跑起来。
Texture Manager 主要解决两个问题:
1. 更好地支持多压缩格式,可以一套 AB 支持多套纹理,用在不同的平台和设备上,提高资产打包发布的效率;
2. 用各种方式降低纹理的显存占用,包括更精细的生命周期管理,还有纹理重映射,来消除冗余,解决一些不合理的加载流程带来的问题。
内存优化
在内存优化方面,先看一下小游戏的内存分布:
1. WASM 编译,这是代码编译和运行时指令优化产生的内存,和 WASM 体积相关,可能会很大。比如在 iOS 上,可能会多到 WASM 体积的 10 倍。
2. UnityHeap,包括:
(1)引擎 native 堆:引擎内部的 native 对象、IL2CPP 运行时;
(2)托管堆:C# 对象;
(3)插件 native 内存:插件(比如 lua)直接调用 new、malloc 产生。
3. GPU 显存
4. 音频
5. 微信基础库和 Canvas
这是具体的注意事项,开发者需要关注的部分,除了常规的那些以外,还有:
(1)首先移除不用的 Unity Package;
(2)然后精简掉小游戏平台不需要的插件,以及去掉重复的库,比如 json;
(3)精简游戏逻辑,去掉小游戏平台用不到的部分,比如多线程,以及前面提到过的 protobuf 的优化;
(4)提高 Code Strip Level,到 High 或者 Extreme,可以用前面提到 Dryrun 功能,提高效率;
(5)使用 WASM 分包,首包大约是原始 WASM 的 30%—50%;
(6)使用 .Net 8 方案,C# 代码直接不进入 WASM。
接下来介绍一下团结新增的 .Net 8 方案。IL2CPP 的一大痛点就是 WASM 过大带来的内存问题,IL 转换成 C++,编译链接进一个大的 WASM,通常会有好几十 MB。我们通过代码剔除、分包、优化代码生成等方式来减小 WASM 体积。
另一个优化方向就是解释执行,就是这个新的 Scripting backend,.Net 8。.Net 8是微软 2023 年发布的,可以稳定支持 WebAssembly,它解释执行 IL,所以可以把 DLL 从 WASM 里分离出来,减小 WASM 体积,进而降低内存。同时我们还可以享受到 .Net 生态的各种新技术,以及未来持续的性能提升。
IL2CPP 内存优化方面,首先分析一下 IL2CPP 运行时的内存开销,其中比较大的部分是 Metadata,这是运行时构建的元数据,我们的优化主要就针对它。之前的实现里,用到某个类型时,会初始化这个类型的完整的元数据,但其中只有很少一部分会实际用到,现在我们对这些元数据进行了延迟加载,其中大部分信息都是可以做延迟加载的,而且粒度可以精确到每个方法。此外我们还裁剪掉了一些平台不支持的特性的字段。下图是一个优化效果的示例:
Shader 内存方面,主要是针对变体相关的优化,思路也是延迟加载。引擎原本会解压所有 blob 数据和加载所有 shader 变体,我们都改为了按需加载,同时及时释放解压后的 blob 数据。实际测试中,BoatAttack 减少了 34.6M,另外一款 MMO 游戏,降幅也可以达到 70%。
引擎底层的内存分配器,存在一定的 overhead,即使在 release 版上也是有的。虽然单个很小,但是架不住次数多,我们把这部分也去除了,同时调整了 alignment。实测下来,在一款中重度的 MMO 游戏上,也可以得到 10M 以上的收益。
Remapper,这是对象序列化位置和 InstanceID 之间的双向映射关系的数据结构。之前是用的 map,加载资源非常多时,map 会很大,但它是稀疏的,浪费比较多,而且不复用 InstanceID,内存增长也比较粗暴。我们现在优化为数组的结构,加上复用策略,更加紧凑,内存增长更合理。这个优化目前只在微信小游戏平台启用,之后会逐步开放到其他平台。
这里展示的是团结 IL2CPP 的优化收益数据,诛仙的案例。WebKit.WebContent 进程内存从 806M 下降到了 755M,减少了 51MB,右边是具体的 breadkdown。
可以看到,团结引擎对小游戏的优化是各个模块一点一滴累积起来的,小到 1、2M,多到几十M,都不放过。
性能优化
在性能优化方面,首先介绍一下小游戏平台的性能特征。
CPU 方面,因为运行在类浏览器环境的 WASM 虚拟机上,算力受限比较多,一般来说是原始 app 的三分之一。同时小游戏目前还不支持多线程,所以部分模块无法得到这方面的加速。GPU 方面,支持 WebGL1 和 WebGL2,相当于 OpenGLES 2.0 和 3.0,基本的渲染能力和原生 App 接近,游戏可以根据自己的情况来选择是不是用 WebGL2,主要是看有没有用到 GPU Instancing、SRP Batcher 这些渲染特性。
具体的性能优化方式,首先看开发者需要关注的点。除了常规的性能优化手段外,小游戏平台有一些特别的点:
1. 不要用 xml、json 解析大文件,解析时候的字符串操作会大量消耗 CPU 算力和引发 gc;
2. 避免用 lua 做重度运算;
3. IL2CPP code generation 和 code optimization,实测对性能影响很大,后面会具体介绍。
引擎侧我们也做了大量的优化,包括 GPU skinning、SIMD,还有很多 shader 相关的优化,稍后会具体介绍。
IL2CPP Code Generation 有两个选项,其中 Faster (smaller) builds,用了 Full Generic Sharing 技术,可以缩短打包时间,减小 WASM 体积,经验数据看能减少约 15%。但是可能会对运行时性能有影响,尤其是对泛型容器。
这里有一些具体例子,WASM 体积都有减小,性能方面要看具体游戏,比如诛仙就没什么影响,但是游戏 2 这个案例受影响就比较明显,所以需要根据项目实际情况来选用。
Code Optimization 选项比较多,它们对应着不同的编译链接选项。需要说明一下的是带 with LTO 的选项,是启用了 LLVM 的 LTO 功能,链接时会跨模块优化,有更多 inline,删除 dead code 更彻底,不过也偶尔遇到过生成的代码异常的情况。推荐的选项是,优先使用 Runtime Speed,需要优化 WASM 大小时可以选 Disk Size,with LTO 的如果项目测试有收益并且也没有异常,就可以用。
接下来看一下引擎侧的优化,首先是 GPU skinning。
下面是 vertex shader GPU skinning 的优化效果,108 个角色的场景,CPU skinning fps 基本在 10 以下,GPU skinning 能达到 30,这样小游戏平台上的 MMO 类型游戏,就能真正做到多人同时在线了。
GPU Skinning
具体看一下各种 skinning 方案的对比。纯 CPU 最慢,启用了 SIMD,速度会有提升,微信分包近期也支持了 SIMD。GPU 方面,传统基于 compute shader 的方式在小游戏平台不支持,我们之前尝试过一个基于 transform feedback buffer 的方案,它对某些场景是有优化效果的,但是对角色数量多每个角色顶点少的情况可能效果不佳甚至是负优化。
最终我们采用了 vertex shader 计算、通过 uniform 更新骨骼矩阵的方案,前面展示的就是这个方案。使用起来也很方便,只要在 shader 代码里添加这样一行就可以,shader compiler 会在编译 shader 时候自动注入相关逻辑,避免用户逐个手工修改了。下面是一个自动修改前后的代码对比。
接下来是 Shader 并行编译。Shader 编译比较耗时,是一直以来都存在的问题。WebGL 不支持 binary shader,单个 shader 编译时间可能需要几十 ms,之前已经有 warmup 功能,但它也不是异步的,只是调整了编译的时机,仍然可能引起卡顿。受益于微信平台的支持,我们利用 WebGL 的这个并行编译的扩展,实现了更理想的 warmup,真正异步起来。一个实测案例下,79 个 shader 变体,总的阻塞时间从 4 秒多降低到了 125ms。
最后是 SIMD 支持。WebAssembly SIMD 提供了和 sse、neon 类似的向量运算能力,iOS 从 16.4 开始支持,团结和微信也很快都支持了。引擎底层的 math 库,天生适合 SIMD 优化,我们重写了它的实现。引擎整体都获得了来自 SIMD 的性能提升,下面是一个 skinning 的例子,耗时下降明显。微信小游戏分包最近也已经支持了 SIMD。
开发提效
在开发提效方面,
1. 前面介绍过 AutoStreaming 和 TextureManager;
2. 团结引擎深度集成了微信小游戏 sdk,切换到微信小游戏平台时,会自动安装 sdk,同时直接把微信打包页面集成到引擎 BuildSettings 里,用起来更方便。
3. 我们还优化了 AB 打包,一个实测案例中,2.5 万个 AB,体积 4G 多,打包时间从 160 分钟减少到 70 分钟。
Dev Host、Frame Debugger、C# Debugging,这 3 个接下来会具体介绍一下。
使用也很方便,参照下图安装这个 package,在 DevHost 这里构建并上传,再使用 Connect App 扫码运行。
微信在安卓设备上可以导出 CPU profile,但是时间精度不足,只有 0.1ms,profile 细节的地方会比较困难。
下图是 Unity Profiler 连微信真机,提供的时间精度更低,只有 1ms。
换成 Unity Profiler 连移动端 Chrome,好一点,但也还是只有 0.1ms。
下图则是我们的 Dev Host,底层使用更高精度的 API,可以达到纳秒级,能看到更详细的堆栈信息,了解更真实的运行情况。
接下来是 frame debugger,这里展示了 frame debugger 连接微信真机小游戏调试的过程,用起来和其他平台一样。
最后再介绍下 C# 调试。Unity WebGL 是不支持 C# 调试的,原因包括多线程受限(需要一个独立的 debug agent 线程)、不支持 socket、无法发送广播、监听端口等等。小游戏平台上,我们的解决方案是增加多线程支持,在 Dev Host 里实现广播和监听,并且增加一个中间代理,桥接游戏 runtime 侧的 debug agent 的 WebSocket 和 ide 侧的 Socket。
下面是具体的用法,打包时勾上 Scripting Debugging,Dev Host 上扫码然后打开这里的开关,在同一个网段内就可以调试了。有一点需要注意,现在暂时还不支持 .Net 8。
下图是一个案例的截图,Attach to Unity Process 之后,就能搜到可以调试的小游戏进程,然后就和传统的调试一样了。
未来展望
赵亮:接下来介绍一下我们目前正在开发中的功能,以及未来的探索方向。
首先看一下独立渲染线程。开发这个 feature 主要目的是降低功耗。苹果开发文档推荐的策略是,将任务集中在一起并发执行。虽然在并发执行的时候功率更高,但任务快速执行完毕后,CPU、内存、缓存、总线都可以进入空闲,所以总能耗会降低。因此使用多线程可以降低功耗。
我们设计了一个对比测试用来验证这个结论。使用同样的工作负载,测试单线程与双线程 demo 的功耗。在测试过程中,把屏幕亮度降到最低,减少屏幕亮度对于功耗的影响,同时退出其他程序,减少干扰。
右边的表格是 iOS Safari 上的测试结果。虽然多线程版本 CPU 占用率更高,但它耗电量降低了很多。每分钟耗电量降低了 42%,电池输出电流平均值从 701mA 降到 411m,电池的温度也因而降了 3 度多。
下面的表格是安卓设备的测试结果,功耗优化程度跟 iOS 相比有所差异。
·小米Note3,使用(骁龙 660 芯片),耗电量下降 7%。
·Nokia7,使用(骁龙 630 芯片),耗电量下降 29%。
虽然在不同设备上功耗优化程度有所不同,但功耗均有可观的下降。
小游戏目前是单线程,为了降低功耗,我们把它改造成了双线程。
具体思路是:游戏中一帧的耗时可大致分为两部分:一部分是游戏更新逻辑、另一部分是渲染逻辑。这两部分的耗时虽然不像测试案例那么理想,刚好 1:1,但是是一个数量级的。因此我们设计成一个线程执行游戏更新逻辑,另一个线程并发执行上一帧的渲染逻辑。
为了实现逻辑和渲染的并行,对引擎架构的改动比较大。主要是渲染相关的数据结构。游戏逻辑的执行,会改变渲染数据,例如物体移动、光照变化。Game thread可以理解为渲染数据的生产者。这些渲染相关的数据,再交由 render thread 去裁剪、组织 render pass、生成具体的 render command。Render thread 可以理解为渲染数据的消费者。两个线程并发,需要处理好线程同步和数据访问策略。既要高效访问,又不能读写冲突。还要处理好数据的生命周期,避免浪费内存。
独立渲染线程预期在今年 12 月会有一个小游戏平台的 preview 版本发布。
接下来看小游戏平台能力的扩展。
微信在 iOS 平台上引入了高性能+方案,使平台对 WebGL 能力进行扩展有了可能。我们第一步将探索、引入部分 GLES3.1 的能力,从而提升小游戏的绘制效率和绘制效果。我们有望在小游戏中也能用上 BatchRenderGroup 和 VFX 等高级特性,甚至用上 GPU Driven 能力从而弥补小游戏 CPU 性能偏弱的天然缺陷。
团结每 3 个月发布一个 feature 版本,在接下来的几个 feature 版本中,我们将逐步推出这些扩展能力的支持。从下面的录屏可以看到,我们已经可以在微信上跑 BatchRenderGroup 的 demo 了。
这里介绍一下宿主侧管理重度资产的思路。把 texture/mesh 这些重度的资产交给宿主进程管理,可以进一步降低小游戏内存占用,减少 oom 的概率;同时提高缓存访问效率、提升加载速度。我们也在考虑把 font engine 转移到 native 侧,既可以减小 WASM 大小,又能通过本地文件系统高效地访问操作系统提供的字体。
Unity 集成的 fmod 版本比较低,不支持 web audio。在 WebGL 平台上,Unity 直接使用 Web Audio API 来播放声音,功能比较受限。在团结引擎上我们使用 MiniAudio 替换了 fmod,MiniAudio 是支持 Web Audio API的,我们正在通过 MiniAudio 来补齐小游戏平台上的声音功能。很快就将推出试用版。
今天先分享到这里,希望广大开发者能用团结引擎开发出更加优秀、更加成功的小游戏,谢谢大家!
长按关注
点击“阅读原文”,解锁 Unite 演讲回放