在 2024 年 11 月 2 日的 Unity User Group 深圳站,Unity 中国引擎底层架构技术主管兼小游戏技术负责人赵亮和 Unity 中国引擎底层架构开发组长詹泽行带来分享《团结引擎小游戏开发指南》。本文为演讲全程实录,请点击文末阅读原文下载 PPT。
很高兴跟大家分享微信小游戏开发的技术。今天会从八个方面来介绍团结引擎对小游戏的技术支持、优化和新功能。首先讲一下游戏案例。从团结引擎今年 1 月 1 日上线到现在刚好 10 个月,目前已经有 400 多款微信小游戏上线,都是通过团结引擎开发的,其中包括重度游戏,如《诛仙手游》小游戏和《远征2》。这是诛仙游戏的画面,左边三个截图是安卓 native APP 的画面,右边是小游戏的画面,可以看到画面上区别不大。这个视频是在微信小游戏的实时录屏,运行还是很流畅的。《诛仙手游》小游戏最开始开发是在 Unity 2022.3 开发的,后来通过跟我们合作转到团结。首先内存直接在 iPhone 14 上从 866MB 降到 785MB,这个优化能够让游戏在 iPhone 8 Plus 这款设备上连续跑 2 个小时,帧率也都是满帧。在红米 K30 Pro 的设备上可以明显看到内存的优化,帧率也有略微的上升。另外一款是客户使用团结之后给我们反馈的游戏数据。切换团结之后很快拿到比较大的内存收益,同时游戏的卡顿率也下降了。最后一个数据是近期跟我们合作的游戏,大概花了两周时间就把游戏上线到了团结引擎。一周时间升级引擎,另外一周开启团结的 GPU skinning,也获得了非常不错的效果,打包的首包大小、内存、CPU、FPS 都有一定的提升。第二是聊一下平台适配,因为很多人没有做过微信小游戏。微信小游戏和传统的 native APP 之间有一些细微的区别,这些区别导致我们在开发过程中可能遇到一些困难。首先内存,微信小游戏内存在 iOS 上限制得非常严格,在比较高端的 iPhone 的峰值内存不应超过 1.4GB,如果超过 1.4GB 就会出现闪退,给用户的游戏体验不好。在优化过程中,需要注意微信小游戏上的内存布局不太一样,它除了传统的 UnityHeap 还有其他的内存需要关注。CPU 这边,因为微信小游戏是单线程状态,多线程加速的计算是没办法做的。可能之前不是瓶颈的地方在微信小游戏这边会成为一个新的瓶颈需要进行优化。Wasm 的执行效率不是很高,是原生的三分之一的左右。渲染能力,微信小游戏是支持 WebGL1/2 的级别,WebGL2 正好对齐到 OpenGLES3.0 的级别,如果是高于 OpenGLES3.0 以上的,比如 Compute Shader 是没办法使用的,导致游戏以前 Compute Shader 做的后处理或者 VFX 都没有办法使用,需要做一些适配。网络通信方面,因为一些安全问题,微信小游戏没有原来的 Socket,需要用 WebSocket 进行替换,基于 Socket 实现的 TCP/UDP 这些协议也需替换为微信提供的 WXTCP/WXUDP。文件系统,传统的 native APP 很多游戏资源放到游戏包里,或者是进入游戏之前先下载下来,后面直接读文件,都是同步操作。但是在微信小程序要进行快速启动,首包非常小,其他资源是后续通过网络进行下载,都是异步的加载,会涉及到游戏同步到异步的改造。另外关于持久化存储文件,小游戏是从 WebGL 过来,WebGL 在浏览器里是一个沙盒的环境,为了安全会限制访问设备本地的文件。所以游戏退了之后有一些想要存储的数据,必须要通过微信提供的特殊接口来存储。除了这些限制之外,我们在平台适配的过程中还需要注意一些工程配置,比如纹理 ASTC 压缩,能很大程度降低游戏的内存。还有 il2cpp code generation 和 code optimization 的优化选项,对微信小游戏性能的影响也是比较大的。 由于微信小游戏是团结这边新增的平台,有些第三方插件及代码也要进行一些简单的设置,比如说设置插件勾上微信小游戏平台,以及微信小游戏中不支持的插件推荐把它删掉,因为它会影响首包及内存。前面也提到内存非常受限,所以有些选项要设置得比较合理,避免因为突然的内存峰值超了 1.4GB,导致游戏崩溃的现象。这里把小游戏的启动时序分成了三块:1. 代码包及资源包的下载。微信小程序首包包含代码包和资源包,第一块优化是如何让这两个包尽快下载下来。我们推荐首包压到 15-25MB,通过 Brotli 压缩将 15-25MB 压缩到 3MB,资源包也是差不多的水平。微信给我们提供的数据是,大多数用户的下载速度大概是 2mb/s,所以一个游戏进入的时间是可以通过包体大小和网速进行反推的。这里是我们提供的优化建议。左边是如何减代码包,通过把多余的 Package 和第三方插件删除掉,以及避免很多 Lua 和 Protobuf 自动生成大量代码。另外需要把引擎里面的 Managed stripping level 调高,让它裁减掉更多的代码。最后是使用 WASM 代码分包工具减低代码。右边是如何让它下载更快。首包资源一般就包含简单的 Logo 和登录页面,这样比较轻量。另外有些文字显示需求,推荐用微信系统字体,这样可以避免文字的下载。也推荐用一些 AutoStreaming 或者是 TextureManager 的工具自动化把资源处理掉,因为有些项目是已经做好的项目,用这些工具更方便节省开发时间。小游戏启动时序的第二部分,这部分主要是引擎的初始化和首场景的加载。下面是 BoatAttack 的启动数据,可以看到它的启动总共是 200 多毫秒。我们推荐首场景的复杂度一定要控制好,因为引擎初始化耗时很短,控制好首场景能够让启动更快。这里是我们对游戏进入启动的耗时分析,最多的耗时是 C# 代码的耗时。第二是渲染进程 Shader 的编译。第三是 Shader 本身的变体比较多,导致数据的解压和解析比较慢。最后是纹理上传,因为小游戏是单线程,它没有办法专门给一个线程上传资源,所以纹理、Shader 的上传会阻塞主线程。我们给的优化建议是,首先优化 C# 代码,建议不要在 MonoBehaviour Awake 以及 Start 构造函数里放太多逻辑,这些逻辑都是在启动的时候首帧画面显示之前执行的。第二,纹理音频要用合适的压缩格式,可以很大程度上减少 CPU 的传输带宽,对功耗和内存都有帮助。第三要减少 Shader 的解析和编译,这里主要是通过减少 Shader 的变体数量。尽量少用 Always include shader,少用 keyword 以减少 Shader 变体。最后是取消 Auto Graphics API,只从 WebGL1/2 中选一个就可以,这样 shader 变体可以少一半。
这里是引擎侧的优化,比如说托管代码精简和引擎代码的剔除,会尽量让 WASM 变得更小。我们打包时加了一个 Wasm 内联优化,这样分包可以把首包分得更小。资源方面也做了默认引擎资源精简以及 il2cpp 元数据精简。还有 AutoStreaming 里面的工具可以帮助轻量的游戏快速上线微信小游戏。右边是我们在运行时加速启动过程的优化。托管代码精简,主要从 Managed code stripping level 入手,我们在 Unity 原来 High Level 之上加了一个 Extreme Level 的级别,它的剔除更激进。之前的剔除不会剔除 MonoBehaviour 的代码,而 Extreme Level 会把 MonoBehaviour 用不到的代码也剔除掉。为了解决等级调高之后脚本丢失的问题,我们新增了一个 Dryrun 的模式,在这个模式下不会真正剔除代码,而是在要被剔除的代码中打一条 log,快速收集可能会被误剔除的代码,通过 link.xml 把它保护起来。这里是我们的 Wasm 内联优化。微信小游戏分包是以函数为粒度,一个函数用了就会保留,没有用就被剔除,如果一个函数非常大,只用了其中一两行就会全部保留起来,这是不太合理的。因此我们做了一个内联优化的控制,通过限制内联让它变成更多的小函数,没有用到的小函数会直接被剔除掉,之后得到的首包也会更小。在一款重度的 MMO 小游戏上,首包从 25MB 降到了 23.7MB,降了 1M 多。这是我们使用的界面,在打包时可以直接拖动进度条。值越少内联会越少,值越大跟没有优化前是一样的。不推荐用太小的值,太小的话函数调用过程比较多,可能会影响运行效率。这是 IL2CPP 元数据精简。IL2CPP 默认的元数据结构可以支持超过 21 亿个类型或方法,但是小游戏实际应用的大概是几万到十几万的级别。所以我们做了元数据精简,打包小游戏时,根据方法的数量自动选择满足当前要求的、精简的元数据结构,这样可以减少 global-metadata.dat 15% 的体积。这是我们在运行时加速优化的数据。第一个是 IL2CPP 的初始化加速减少了 24ms,第二个是 GLES Device 的初始化也优化了 24ms,Shader 变体及 Monoscript 也减少了十几毫秒,整体大概减少了 100ms 的样子。小游戏启动的时候,经常要求快速进入游戏,所以边下边玩是比较重要的功能模块。对于一些大游戏来说可能有些框架用得比较好,直接用 Asset Bundle 边下边玩也可以做到比较好的体验,但是这里有一个问题是 AB 的加载时机、拆分粒度可能不太好,比如说游戏内单个 AB 有 30-40MB,用户点击按钮后就要加载很久。依赖关系和拆分 AB 的大小、粒度都是比较重要的点。关于 AB 依赖不合理的问题,有遇到过一个 AB 依赖几十个上百个 AB,这对于下载等待以及内存 footprint 都是灾难。比如可能只需要一张纹理,但这个纹理所在的 AB 依赖了很多个 AB,导致原本只需要下载 1-2MB,变成不得不下载几十 MB。团结有两个工具帮客户快速做边下边玩,一个是 AutoStreaming,在打包时将 AB 的重度资源放到云端上,运行时会自动把这些重度数据加载回来。这个工具主要针对比较轻量的微信小游戏,重度的游戏推荐自己通过 AB 的机制优化。另外一个工具是 Texture Manager。微信小游戏有一个比较尴尬的难点是 PC 上和手机上支持的纹理压缩格式不一样,而且没办法兼容,PC 只能使用 DXT,手机只能使用 ASTC,如果不符合这个要求就导致内存比较多,体验不好。所以 Texture Manager 首先支持的是一个纹理可以打印多套纹理压缩格式,运行时自动选择,比如 PC 上使用 DXT,手机上使用 ASTC,不会导致性能问题。二是 Texture Manager 会管理纹理的生命周期,纹理依旧存活但是没有使用的情况下,会把纹理的数据从 GPU 上踢下来,节省 GPU 的显存。Texture Manager 也会在打包时做纹理重映射,我们发现不合理的 AB 组织结构或加载方式导致纹理重复造成浪费,Texture Manager 会进行重新处理,相同的纹理只会出现一张,不会重复。第五是内存优化,内存优化是小游戏上线最重要的一个点。这是小游戏的内存分布,和传统的 native APP 不太一样,除了比较熟悉的 UnityHeap、GPU,小游戏独有的内存还包括 WASM 编译、音频、基础库和 Canvas。音频、GPU 显存、UnityHeap 及 WASM 编译这四块是我们比较能够控制的内容,接下来会聚焦这四块的内存优化。首先是 Wasm 瘦身。Wasm 大小直接关系到编译的内存,如果 Wasm 是微压缩状态,10MB 的 Wasm 大概占运行时内存 70-100M,大概是 7-10 倍。我们推荐要对 WASM 瘦身,一般推荐上线游戏首包 Wasm 不要超过 20MB,这是比较上限的位置。在 AB 加载这边也经常看到用户踩到一些坑,使用 Unity 默认的 Web request 下载,导致有一部分缓存长期驻留在内存里没有清理,这里推荐直接把下载机制换成微信的 WxAssetBundle,可以自动把内存没有使用的踢掉。对音频、纹理这些资源要设置好分辨率和压缩格式。微信提供了一个 Performance+ 方案,原来小游戏是 GPU 的显存和内存是在同一个进程里面,苹果这边是单个进程如果内存超过了 1.4GB 就给你杀掉了。而 Performance+ 会把 GPU 的渲染放到另外一个进程,相当于把把两个内存放到两个进程,单个进程的内存上限有提高,可以让小游戏被杀的概率减少。引擎侧我们也做了很多具体优化,来降低游戏的内存,接下来我会详细讲讲。这是一个案例,诛仙这款小游戏我们是从头跟到尾的,所以比较清楚它的数据。最开始 Wasm 有 90MB,通过移除不用的 UnityPackage 和第三方库降到了 70MB 多。经过精简 Gameplay,调整游戏里的 Code Strip 设置和优化设置,最终降到了 51MB左右的 Wasm ,用的是 il2cpp 方案, 经过微信的分包,最终得到首包的 WASM 是 20MB 多一点。如果切换到 .NET8 方案的话,因为 .Net 不会把 C# 编译成 WASM,WASM 会进一步降低到 14.6MB。.NET8 是团结引擎特有的 backend,Unity 只有 IL2CPP,所有的 C# 都会转化成 C++ 代码,再编译成 WASM。.NET8 直接把 DLL 通过容器封装成 wasm 的格式,本质上还是 DLL,通过 WASM 上跑一个虚拟机来执行 DLL。.NET8 这个平台在内存方面优势是比较大的。这里是 IL2CPP 内存优化的实例,可以看到这款游戏 il2cpp 总的内存占用是 63.89MB,元数据的占用是 37MB,通过 il2cpp 元数据延迟加载的优化,比如说把暂时用不到的 Method 等延迟加载,可以把元数据的内存从 37MB 降到 13MB。这是 Shader 内存的优化,以前引擎加载 Shader 的逻辑主要是把它所有的变体都加载进内存,为了在微信小游戏上减少内存,修改了逻辑,只是创建一个空的 Shader 变体,真正编译 shader 的时候才从 Blob 数据把 shader 加载进来,这个优化在 Boat Attack 中减少了 30 多 MB 的内存。接下来是内存分配器优化。Unity 引擎内存分配器,会记录每一次分配的信息,并且在这个层面引发多一次对齐,即使是在 release 版本也是如此。微信小游戏内存非常紧张,为了节省开销进行了优化,把一些 Overhead 去掉,对齐也从 16 字节优化为 8 字节,在中重度游戏上也能够减少 10-12MB 的内存。这是 Remapper 的优化。Remapper 是一个表,它存的是对象序列化的位置和 InstanceID 之间的双向映射关系。这个表如果同时加载资源非常多,就会非常大,内存可能达到 70-80MB。之前的策略是 InstanceID 每次加二不复用,而且存在哈希表里,这个哈希表是稀疏结构,内存是翻倍的增长过程。我们把它优化成了数组,它是紧凑的结构,每次都是线性增长。这里可以看到优化后是 3MB。可能原本的大小就是 4MB,现在不够了,如果是 MAP 就增长到 8MB 去了,但是数组可能只是 4.1MB 的增长幅度,不会有太多的内存空间浪费。这是总的内存对比,左边是小游戏当前进程的内存,从 Unity 2022.3.13f1 切到团结 1.1.3 之后,内存大概是降低了 55MB。右边的表格是通过前面每个细节项对比拿到的数据,总计是 66MB,每次运行内存都有一些小波动,基本上和我们的数据吻合。这个是从团结的 Il2CPP 切到团结 .NET8 平台的内存对比,可以看到内存直接从 755MB 降到了不到 600MB,降低了 150 多 MB。右边的表格是预估内存的收益,能解释大概 130MB 左右,其他是内存波动带来的差异。第六是性能优化。前面讲过 CPU 没有多线程,以及 WASM 执行效率不高,所以性能的优化也是非常重要的。WASM 的执行效率是原生 APP 的三分之一,这是我们找到的数据支撑。渲染方面,除了一些渲染 Feature 不支持外,渲染本身执行效率和原生 APP 的差别不大,但是功耗方面可能会有一些上升。我们提供了一些优化建议。CPU 这边,我们也看到了一些游戏的 CPU 卡点。首先是解析游戏的配置表,如果是 XML、JSON 的结构解析是比较费时间的,而且微信小游戏没有多线程的情况下,可能会卡几秒钟,推荐把配置表直接转二进制的形式去读会快很多。另外,有些游戏 LUA 逻辑比较重,到微信小游戏相当于是多了一层虚拟机,执行效率是比较低的,所以避免使用 LUA 做重度计算。另外还有两个引擎里面的优化选项:Il2CPP Code Generation 和 Code Optimization,这两个选项对小游戏的性能影响比较大,所以我们会单独拿出来讲讲。引擎侧也做了很多优化让性能得到提升,包括 GPU Skinning、Shader 并行编译,也会在渲染方面优化,比如优化 glInvalidateFramebuffer 调用顺序,清除多余的 GL 调用等。这里重点提一下 IL2cpp Code Generation 在 player settings 中的两个选项, Faster runtime 和 Faster (smaller) builds。如果选择 Smaller,它会用 Full Generic Sharing 技术缩短打包时间,减少生成的 wasm 体积,这也可能会带来一些性能的回退。如果你的游戏是比较注重性能的话,可以选择 Faster runtime,如果更看重游戏包,本身性能还可以的话,比较推荐使用 Smaller Builds 的选项。这是 Code Optimization 的选项,右边列出了选择不同选项的区别,我们的编译参数、链接参数都不太一样。我们推荐使用的是 Runtime Speed 的选项,至于 LOT 推荐可以去试一下,如果有异常可以回退到 Runtime Speed。如果游戏的 WASM 真的特别大,而且性能还可以的话,也可以选择 Disk Size 这个选项,可以让 WASM 的体积变小,内存也会好一点。这是在诛仙做的 Demo 对比,左边是 CPU Skinning,因为微信小游戏没有 compute shader,所以说原来基于 compute shader 的 GPU Skinning 这个方案是做不了的。我们通过 transform feedback 及 vertex shader 支持了两种 GPU skinning 方案,差别很明显,左边不到 10 帧,右边基本上可以跑到 30 帧的水平。
这是关于 Skinning 的介绍,GPU-Vertex Shader 和 GPU-Transform Feedback 是在团结上新支持的两个选项。开启方式也非常简单,Transform Feedback 直接打开就好了,Vertex Shader 就需要在 shader 代码中改一行代码。右上角的图是需要修改的操作,加入一行代码,下面的图是修改完代码,引擎的编译器就自动处理成下面的结构,自动支持 GPU Skinning。还有一些注意事项。目前 Vertex Shader GPU Skinning 是我们比较推荐的方式。Transform Feedback 方案在模型比较小、数量比较多的时候,我们确实观察到了性能回退的现象。Vertex Shader GPU Skinning 有一些限制条件,首先是动画骨骼不能超过 64 根,我们现在也在拓展,当前如果超过了需要做一些拆分。引擎内部的 Shader 大部分已经自动支持 Skinning,用户这边的 shader 就需要加一行代码。GPU Skinning 方案和原来的 CPU skinning 是可以共存的,可以只改一部分比较重度的代码,有些不好改的继续用 CPU Skinning 方案,渲染还是一样的。最后一个注意点是一个 renderer 上可能有多个材质,如果要支持 GPU Skinning 需要把每个材质所使用的 Shader 都需要修改才可以,否则会回退到 CPU Skinning 的状态。这是我们对 shader 并行编译的优化。前面也提到 shader 编译单线程,不支持 binary shader 的问题导致编译比较慢。引擎本身是支持 Warmup 的接口,但是因为 WebGL 这些缺陷,可能调用一个 warmup 接口会卡 4s,体验非常不好。我们是基于 WebGL 的 KHR_parallel_shader_compile 拓展,去做了一个 shader 并行编译,相当于我们只是提交了一个 shader 编译,主线程就可以离开去做其他事了,过段时间再来检查这个 shader 状态就好了。这样对主线程的阻塞时间从 4s 降低到 100 多 ms。这是 SIMD 支持,目前引擎大多使用的还是标量运算,对一些性能不是很友好。所以我们在 Math 库上支持了 Wasm 的 SIMD。为什么选择 Math 库呢?因为引擎里包括 transform、动画、粒子等很多都依赖 Math 库,这样可以让引擎整个都能享受到 SIMD 优化的好处。需要注意的是 SIMD 在 iOS 上是 16.4 版本上才支持,如果微信小游戏用户群覆盖广,可能这个功能暂时还不能用上。异步实例化, Unity 2023.3 里面加了这样一个接口,团结针对这个接口做了一些修复,特别是针对微信小游戏的处理。首先同步 instantiate 是单帧执行、单帧完成的。第一步是反序列化、构建对象。第二步是初始化对象,包括 script Awake, upload texture, upload mesh 等耗时操作。如果是用 InstantiateAsync 异步就是把这两步拆分到两帧去做完。如果它是在 native 平台,可以通过 job 加速对象的构建。但是微信小游戏的环境比较特殊,它是单线程, JobSystem 无法加速,另外上传的资源没有办法放在另外一个线程,所以 Integration 这步耗时会相对更长。所以这里针对这一步做了一些拆分,让它可以在 integration 这步分成更多帧执行。拆分的过程中,会确保子任务执行的顺序和原来一模一样,不会出现顺序的问题。这里面也会涉及到部分对象之间是有关联的,比如说粒子和 Collider,所以还是有切分粒度的问题。但是脚本和资源是可以切分到很细,单个脚本和单个资源都没问题的。这是我们在实际案例上测试的数据。上面同步是在单帧内 73 毫秒执行完,异步是 4 帧把 instantiate 做完。总结起来数据是左边的表,可以看到如果是同步实例化是 73ms,异步是 84ms,如果从单帧最高时间及平均每帧时间来看,总共 7 帧,同步单帧卡顿最长,平均每帧率耗时也更高一些。所以异步实例化解决了两个问题,一是可以减少游戏的卡顿,二是一定程度上把平均帧率提上去,可以把工作分摊到更多的帧里。右边是用真实游戏测出来的数据,通过 InstantiateAsync 改造之后,卡顿率和 FPS 都有所改进。首先,1.3 版本新增了 Wasm 分析工具,用户可以在打包时勾选一个选项,或者是打包之后读 Wasm 文件,告诉用户哪个模块占了多大的体积。打包时可以给用户一些选项,两次打包可以进行对比,发现哪些 Wasm 是可以优化的。现在还只支持完整的 wasm 包,未来我们还会支持经过微信分包之后的 Wasm,并且还可以进行 wasm 差异对比,以及 wasm 函数名的排序、查找功能。另外是我们做了一个 Dev Host 安卓的小游戏宿主,比微信这边有三个优点:高时钟精度 Timeline profile、支持 Frame Debugger,还可以做一些代码调试。以前微信 WebGL 平台做不到像 Frame Debugger 和 C# Debugging,受平台限制没有办法实现这些功能。启动这个工具也很方便,直接在 Package Manager 里搜索 cn.tuanjie.minigame.host 即可。点击构建并上传后,使用 Connect App 扫码即可运行。这里说一下我们的时钟精度,这是我们在微信开发者工具里面拿到的性能数据,可以看到这里有一个算矩阵的逆的操作,花的时间是 0.2ms,这是个很简单的算法,不可能花到这么多,所以说这个信息可能会对优化函数造成误导。可能只是采集性能数据时刚好采到这个点,认为后面所有的 0.2ms 都是这个函数消耗的。这是我们对比了几个不同的平台看时间精度,微信真机上通过 Unity profiler 去连接,拿到的时间精度最多是 1ms;移动端 chrome 是 0.1ms;如果到 Dev host 宿主里面是 0.001ms。这三个图都是同一个函数的时间调用,时间精度上升之后就能看到比较细的函数的调用时间,帮助我们优化。这里是 Frame Debugger 的视频,目前支持在微信小游戏、安卓及 iOS 使用 Frame Debugger 的能力,来查看比如有没有 batch 和渲染出错等问题进行调试。最后是 C# Debugging,之前有用户抱怨我们做微信小游戏查 Bug 只能打 log,所以我们加了一个 C# Debugging 的能力。之前为什么不支持这个能力是有原因的,首先 Debug 需要有一个调试的 Agent,所以要有多进程。另外像 visual studio、Rider 接受消息都是 Socket 协议,但 WebGL 不支持 socket,只支持 Websocket,两边协议是不兼容的,没办法直接建立连接。还有一个是 Websocket 没办法监听、广播。针对微信小游戏平台,我们首先增加了多线程支持,让 Debug Agent 在单独的线程上运行起来。然后在小游戏宿主中新增一个中间代理,一边通过 Socket 去跟 IDE 交互,另一边通过 Websocket 跟 Debug Agent 交互。这个中间代理具备广播和监听端口的能力,让 IDE 可以方便的搜寻到需要调试的小游戏。使用起来也比较方便,首先要在打包的时候把 Script Debugging 给勾上;在宿主上需要去点一下打开 C# 代码调试按钮。同时确保 IDE 和宿主运行在同一个局域网内,就可以建立连接,进行代码调试了。现在暂时不支持 .Net8 scripting backend。这是使用过程中的截图。左图显示在 Rider 中点击 Attach to Unity Process 后,就能搜寻到可以调试的微信小游戏进程。右图显示 IDE 调试窗口,可以看到当前中断的代码行,变量的数值,以及执行的堆栈。接下来介绍一下我们正在做的工作,以及接下来的计划。现在做的工作主要是独立渲染线程。刚才提到很多小游戏上只有单个线程,带来的问题是主线程时间消耗很长、帧率比较低,而且会带来功耗的问题,不能发挥出多核优势。同我们还在做 GPU Instancing,之前选择了 SRP Batch,它的优先级是高于 GPU Instancing 的,会导致虽然 set pass 比较低,但是它 draw call 数还是比较高的,这也是一个值得优化的地方。还有动画的压缩,用了 ACL 的压缩之后,可以使 Animation clip 变得更小。同时我们也对 C# 的编译器在做一些优化。再有就是平台渲染能力的拓展,是我们跟微信一起在探讨的,如何把小游戏平台的渲染能力进一步提升。接下来给大家详细介绍一下这几点。首先是独立渲染线程。这是当前的进展,目前可以支持 Mesh Renderer、Skinned Mesh Renderer、UI canvas 都是可以支持的。测下来在 iOS 和安卓上都有比较明显的功耗下降,在安卓上有 FPS 的提升。预期在明年上半年发布。大家看一下具体的测试数据,在 iPhone 14 上,原本单线程的执行时间是 2.8+4+4.7ms,双线程的整体时间消耗多了一点,但是看一下测试电流的功耗反而下降了,因为核降频了,降频之后运行的效率更高。在天玑 9300 上,开了双线程之后,把渲染单独拿到一个线程上执行,整体这一帧 frame time 的是降低的,帧率有所提升,同时功耗下降。在骁龙 865 上跑,也是和天玑 9300 类似的结果。Frame time 降低,同时功耗下降。这对小游戏来说是帮助挺大的 feature,同时也需要微信那边配合我们把小游戏的多线程环境支持做好,目前的方案已经有了,微信正在做基础设施的建设。这是独立渲染线程大概的原理。小游戏上没有像在原生 APP 上一样搞很多 thread,目前在 WebKit 和 V8 的环境中每多跑一个线程,内存的开销增加都蛮高的,不像原生的 APP 上多增加一个线程都是 free 的情况。所以我们只增加一个线程,减少内存的增量,同时获得多核的收益。需要注意的地方是采用独立渲染线程之后,Game Thread 和 Render Thread 并行处理的不是同一帧的,当 Render Thread 处理第 N 帧的时候,Game Thread 已经处理了第 N+1 帧,所以说我们需要把第 N 帧的渲染场景做一个快照,从 Game Thread 传到 Render Thread 去,使得 Game Thread 处理第 N+1 帧率的时候,Render Thread 处理第 N 帧,它们相互之间不会有冲突。接下来介绍 SRP Batch 上增加了 GPU Instancing 的支持。这个 feature 我们预期在团结的 1.4.0 即 2024 年 12 月底可以发布出去。右侧有一个 profiler 的结果,打开了 GPU Instancing 之后,我们的 Setpass 数是差不多的,但是 Drawcall 数会大幅降低。假如不开 Instancing,对于有些场景 instance 数量特别多的情况下,drawcall 有可能到 5000 以上,打开之后可以通过 Instance 的方式绘制,这样 Drawcall 的数量降幅会非常明显,变成之前六分之一的样子。无论是 iPhone,还是安卓,高端和低端设备上都能看到 FPS 有明显的提升。对 C# 编译器我们也做了优化。一是做更好的增量分包,比如小游戏发了一个包,隔一段时间对代码做了一个小的改动,需要再更新一个版本。这个时候我们不希望重新做一次采集分包的过程,因为采集分包比较耗时,而是想利用第一次采集的结果。这意味着做少量代码改动时重新编译生成的 WASM 里面的函数签名需要很稳定,这样原来采集的信息才是有效的。Unity 原本用的 Rosyln 编译生成出来的函数名不是很稳定,主要是 Lambda 的表达式里有一个 MethodOrdinal,编译器对于一个函数假如有重载,在里面会通过数字标识不同的重载,这个数字生成的时候不稳定,导致可能代码压根没有改动,但是两次编译函数生成的签名不一样,导致采集 broke,这一块我们打算修掉。第二是,用 foreach 遍历 List,跟不用 foreach 来遍历 List,会发现这两个生成的 Wasm 代码差异非常大。发现 foreach 在 Rosyln 编译的时候会进行展开,里面会增加一个 try-catch。假如自己从 list 拿一个 enumerator 进行一次循环的话,生成的 WASM 大概只有 100 多条,但是如果套了 Try-catch 之后,再调取 disposable,就会有 500 多个 instruction。当一个游戏里大量使用 Foreach,会导致带来很多 Wasm 的体积膨胀及运行时内存增加。Try-catch 很多时候发现它不是必需的,所以我们准备在 Rosyln 的编译器里面给它一个选项,当你选择不需要 try-catch 的时候就可以优化掉。动画压缩,主要是集成 ACL 库,它有更高的压缩比。用一个 animation clip 进行对比,不开启压缩占了 327K,原本通过 Keyframe Reduction 或者是 Optimal 的方式能降到 62K,用了 ACL 之后能够降到更低,是 Optimal 的六分之一,只有 10.3K。这对于小游戏特别有用,让打包的 animation clip 资产变小,使得加载更快,包体更小,同时运行时内存也有相应的降低。ACL 对于 animation clip 的 sample 也会更快。我们预计也在 12 月底发出这个 feature,性能收益也会整理出来。渲染能力的拓展,这是我们跟微信共同研究推进的地方。目前的思路主要有三个:1.在当前的 WebGL 里增加对 computer shader、SSBU 或者是 indirect draw 的支持。2.对 WebGPU 的支持,使得更新的 Graphics API 得到支持;3.尝试让小游戏直接使用 Vulkan/Metal。这样有很多好处,可以不再受限 WebGL 2.0,也不局限使用 WebGPU。因为 WebGPU 本身走向成熟需要一定时间,而 Metal 当前就是成熟的。另外在使用 WebGPU 的情况下,手机底层驱动还是 Metal,需要一个 WebGPU 到 Metal 的转换过程,它不是 Free 的。我们跟微信探讨直接在 Performance+ 这样的方案上支持 Metal,通过这种方式,未来大家就可以在小游戏上使用上 Visual Effect Graph 和 GPU Resident Drawer 这样的功能,小游戏上绘制的场景天花板会逐渐增高。这个功能有可能会到明年的 Q2、Q3 发布,需要我们跟平台侧把能力共建起来。最后欢迎大家扫码加入小游戏官方交流群,如果有问题可以在 QQ 群里沟通。