2024 年 3 月 31 日,在 Unity User Group 广州站上,Unity 中国工程师赵亮、詹泽行、傅有成做了《团结引擎 1.1.0 版本小游戏新功能&优化》内容分享,介绍了团结引擎 1.1.0 版本小游戏相关新功能,以及未来的工作方向。
本文为演讲全文记录,请点击文末阅读原文下载 PPT。
赵亮:大家好!我是 Unity 工程师赵亮,今天跟大家分享一下团结引擎 1.1.0 版本小游戏的相关的新功能。首先是 DotnetWasm 方案,避免了之前把 C# 代码都翻译成 CPP 之后,Wasm 体积太大,消耗过多内存的问题。
还有 GPU Skinning,之前我们提供 Transform Feedback Skinning 方式,但是那个方案有一些地方会有负优化,所以针对之前负优化的情况新增了 VS GPU Skinning。还有 Shader Warmup,之前的 Warmup 操作会卡住主线程,有的时候会卡五六秒,现在我们能把主线程卡顿优化到 1 秒以下。C# 的代码过多会导致 Wasm 很大,所以新增了 Extreme Level,可以帮助剔除用不上的 C# 代码,比现有的 High Level 剔除更多。同时还新增了 Dryrun 模式,在该模式下,执行到待剔除函数时会打印警告日志,帮助开发者快速定位出来哪些类型或方法需要 Preserve。在此之前很可能大家试了一下 Medium 或者 High Level 剔除,然后游戏 crash 了。开发者难以定位错误,便改回 Low Level,Wasm 体积就会很大,浪费很多内存。还有 IL2CPP 元数据精简,微信有个 Wasm 分包方案,可以给 Wasm 瘦身,但无法对 global-metadata.dat 这个文件瘦身。针对这个文件,我们也做了特殊的优化。然后还有一些内存分配器相关的优化,之前的内存分配器更多是针对原生的应用优化,原生应用内存比较充裕,可以更多的采用空间换时间。而小游戏内存很紧张,分配策略需要相应的调整。还有 TextureManager,可以更好管理 Texture 的生命周期,节约内存。小游戏宿主增强了对调试的支持。之前 WebGL 不支持 Frame Debug。在宿主里,我们增加了 Frame Debug、C# Debug 的支持。在浏览器里是很难做 C# Debug 支持的,因为 C#的Debug 依赖于多线程、广播、socket,还依赖于主动去监听端口。浏览器因为安全等原因,不支持以上能力。通过我们开发的小游戏宿主,开发者可以像 Debug 安卓、IOS 等原生 APP 一样去 Debug 小游戏。然后还有一些比较细节的,如 Remapper 的运行时内存占用优化。还新增了 Math库对 SIMD 的支持,之前我们针对 Mesh Skinning 做了 SIMD 的支持,后来研究发现 Mesh Skinning 最好还是应用 GPU Skinning。在 Math 库支持 SIMD 之后,上层使用 Math 库的这些模块都会受益。最后我们还深度集成了微信小游戏 SDK,大家在团结 1.1.0 版上切换到微信小游戏平台后,会自动下载合适的 SDK 版本。以上是 1.1.0 版本新增的功能与优化。接下来简单介绍一下未来的工作方向。我们将适配轻量化的物理引擎 Bullet,这个功能的开发已经接近尾声了,预期在一个月之后发布 Beta 版。然后是引擎公共包,正在与微信合作开发中。引擎里的常用模块将构建成一个共享 Wasm,由微信提前下载准备好,从而进一步减小小游戏的首包。多线程支持,之前的分享中有提到过。由于开启多线程会带来内存的增加,导致这个方案进展比较缓慢。下一步的计划是先实现双线程,把 game thread 与 render thread 分开。假如游戏的 1 帧耗时 20 毫秒,game update 可能会占 10 毫秒左右,render 会占剩下的 10 毫秒。我们计划把 render 工作从主线程移至 render 线程,从而降低功耗与发热,稳定游戏帧率。设计对比测试实验,同一个任务,分别用单线程、双线程去跑,各执行 100 分钟。单线程版本的电池电量降了 38%,双线程版本电池电量只降了 20%。在 IOS 设备上查看 CPU 的电流,一个是 700 多毫安,一个是 400 多毫安。所以说同样的任务,采用多线程去做的话,可以让电池续航更持久,发热问题得到减缓。否则,设备发热之后帧率会不稳定。在引擎侧更加充分利用宿主的新能力。大家可能知道微信近期发布了 Performance+ 方案,把一些渲染工作放在宿主的 native 侧做。我们在考虑未来是不是可以支持 Shader 的 Binary 加载。在这一版的 Texture Manager 中,纹理加载的关键过程还是在 Wasm 这一侧进行处理的,未来可以探索把纹理完全放在 Native 侧做。Wasm 侧仅仅只是发送 command,这样可以减少 Wasm 内存和 CPU 的占用。GPU Driven 的管线对于小游戏特别合适,因为小游戏的 Wasm 的计算能力非常受限,如果把绘制的任务更多的分配给 GPU 做,可以进一步提高小游戏性能。Texture Manager 是针对资产打包重构的第一步,接下来会逐步重构 Mesh、Audio 以及其他资产打包管理。在双线程基础上,未来我们还将探索增加更多的线程。这里需要权衡 CPU 的收益与内存的开销。有可能需要把引擎里面一些特殊的使用多线程的模块剥离成单独的 Wasm Module,才能控制好内存使用。接下来介绍一下 1.1.0 具体新增的功能。
首先介绍的是 Dotnet Wasm 方案,现在微信小游戏方案最大的痛点就是内存,造成内存问题的一个很重要的因素是 Wasm,IL2CPP 方案下 IL 被转化成 C++,然后编译链接成一个大的 Wasm,经常会有好几十兆。浏览器在加载执行 Wasm 的时候,因为代码编译或者优化等原因,会消耗更多的内存,这个量可能会是 Wasm 本来体积的几倍甚至可能多至 10 倍,比如一个 50M 的 Wasm,仅在这方面就消耗 500M 内存,剩余给游戏的就不多了。直接精简 Wasm 的体积,这个相对比较困难,我们也正在推进。同时我们也从另外一个方向来解决内存问题,就是新增了一个 Scripting Backend: .NET8。去年微软最新发布了 .NET8,已经可以稳定支持 WebAssembly。基于它我们开发了这个 backend,在这个 backend 下面,IL 是解释执行的,可以从 Wasm 里剥离出来,Wasm 的体积会大大减小,相应的运行内存也就下降很多。同时我们还可以享受到 .NET 生态的很多的新技术,并且还会有持续的性能提升。
接下来具体介绍一下 Dotnet Wasm 方案的原理流程。首先是构建环节,最开始的 DLL 编译和 Strip 步骤,Dotnet Wasm 和 IL2CPP 是一致的,接下来从 DLL 处理步骤开始有区别。Dotnet Wasm 方案下,DLL 只是简单的被打包成一个 WebCIL 格式的 Wasm 文件(这是一种符合 Wasm 规范的二进制容器格式),再接下来的步骤是 native 代码的编译和链接,会把 native 代码加上引擎模块再加上 Dotnet Runtime[xr1] ,最终生成一个 dotnet.native.wasm,以及一些其他 js 胶水代码,这个 Wasm 相比 IL2CPP 产出的 Wasm 体积就小非常多了。运行时的启动流程也会有一些区别,最开始先加载一些引导文件,包括 Dotnet 的 Data 和 JS,接下来下载代码资源,首先是 dotnet.native.asm,它和之前的 Wasm 一样,是在小游戏包里的,从微信服务器下载。然后下载 WebCIL 文件,这些是从 CDN 下载。既然是从 CDN 下载,那就已经具备了热更的可能性。接下来的步骤是初始化,首先是初始化 DotnetRuntime,然后以 Buffer 的形式加载 WebCIL 文件,只是把它整个读进内存,但是并不做解析,然后加载 dotnet.native.wasm。再接下来就是正常的启动游戏的流程了,之后游戏运行的时候,需要用到 C# 方法的时候,会按需加载对应的 WebCIL 文件,执行里面的方法。以上就是 Dotnet Wasm 方案的基本原理和流程。关于可以享受到的 Dotnet 生态的新技术,SGen GC 就是其中之一。当前 IL2CPP 使用的 Boehm GC 是一个通用的、面向 C++ 的 GC,它用在 Mono 上就没有那么有针对性。SGen GC 则是专为 Mono 设计的,它可以精确扫描 Mono Stack,还可以在运行时移动整理内存碎片,而且它是分代回收,回收行为可以分散在每一帧,这样可以减少卡顿,不像 Boehm GC 是定期的全量 GC。SGen GC 的缺点在于,会有一些预分配的内存,之后可能还会按需上浮。开发者可以根据自己游戏的情况来选用,内存比较紧张的,仍然可以使用 Boehm GC,如果内存不紧张的话,可以尝试新的 SGen GC。
另一项新的技术是 JITerpreter。它是一个类似于 JIT 编译优化的特性,是把部分的热点代码转成细密的 Wasm 代码,,来提高运行效率。对于解释执行的程序,很显然它可以带来很可观的性能提升。同时它也可以用来提升 Wasm 和 JS 代码之间的相互调用。这项特性在 .Net8 的 Scripting Backend 是默认开启的。总结一下 Dotnet Wasm 方案的优势就是:可以有更小的运行内存,更平滑的帧率波动,出包也会更快,有方便的调试体验,还有天然支持热更新。这是一个实测案例的数据,是一个重度的 MMO 游戏,可以看到 Dotnet Wasm 的方案内存的下降是非常明显的,这样的内存用量可以保证在 iOS 上稳定运行很久。未来 Dotnet Wasm 方案我们还会一直持续开发下去,一方面是一些细节的完善,比如现在有少量的 API,因为 Dotnet8 的限制还不支持,之后会补上或者提供替代方案。引擎、微信小游戏的各种细节的功能,也都会全部完善支持起来。之后还会有更多的新技术加入,比如 AOT。Dotnet Wasm 方案可能还会扩展到更多的平台上。
接下来介绍的是 GPU Skinning,在微信小游戏上原有的一些 Skinning 的方案不太好用,比如 SIMD,微信小游戏的分包现在还不支持。GPU 最常用的 Compute shade,WebGL 不支持。团结早一些的版本,我们开始提供一个 Transform Feedback 方案,增加了一个 PASS,利用 Vetex shader 计算,输出到 Transform Feedback Buffer,然后回读回来。但是这个方案比较挑场景,对于角色多,或者每个角色顶点数少的场景,效果不太好,甚至可能是负优化。
所以这次提出了一个新的 Skinning 的方案,大致的原理是:Skinning 还是在 Vertex shader 里计算,但是不增加 pass,不回读,直接把数据用在后续的 MVP 坐标转换。这个方案用起来也比较方便,只需要开发者少量的介入,对于引擎内置的 shader,已经直接全部支持了,用户自定义的 shader,只要加上这么一行就可以了。这是性能的对比,是和刚才提到的几个 Skinning 方案的对比的数据,统计的是 Skinning 的耗时,可以看到 Vertex Shader Skinning 的性能优势是很明显的。
下一个介绍 Shader Warmup。Shader 的编译比较耗时是一直以来都存在的问题,WebGL 不支持 Binary Shader,单个 Shader 的编译时间可能要好几十毫秒。现有的 Warmup 并不是异步的,只是把编译时机换了一下,例如全部放到游戏最开始加载的时候,即使是 Progressively 也只是分散开了,但是仍然可能影响 FPS。
其实最理想的方案异步的 Shader Warmup,WebGL 有扩展可以支持 Shader 编译异步化,基于这个扩展我们就实现了异步的 Shader Warmup 方案,使用很简单,首先调用接口发起编译,然后就可以执行别的任务,同时可以查询进度,在进度完成之后,最后再 Check status。
这是在一个案例上实测的数据,原先直接 Warmup,完整的编译时间要 6 秒多,现在用异步 Shader Warmup 的方案,最开始发起编译耗时 120 毫秒,最后 Check status 耗时 700 多毫秒,总和相比原来减少了 86.5%。Managed code stripping
Extreme level
接下来是,Managed code stripping 新增了 Extreme level。Managed code stripping 是通过 UnityLinker 把 DLL 里面没有用到的代码剔除掉,以此减小体积。原来最高的级别是 High,但是这个级别的策略还是比较偏保守的,很重要的一点就是它保留了 MonoBehaviour 和 ScriptableObject 的全部成员。即使是这样,它还是有可能会剔除掉一些我们需要的代码,这个就只能用户自己来发现,运行时反复遇到异常再添加 preserve,非常痛苦。我们这一次新增的 Extreme level,用了更激进的剔除规则,主要就是针对 MonoBehaviour 和 ScriptableObject,原先是全部保留,现在只保留 Unity Event Functions,例如 Awake、Update,以及其他实际会用到的方法,其他的都会剔除。Extreme level 下同样也可能剔除掉实际要用到的代码,所以我们增加了 Dryrun 模式,在这个模式下面代码不会真的被剔除,而是在待剔除的方法里面插入警告的 Log,游戏运行的时候,待剔除的方法被调用到了,会打一句警告的信息。通过这种方式,开发者可以一次批量的快速的把这些误剔除的方法全部收集起来,这样效率就高了很多。
而且对 IL2CPP 和 Dotnet Wasm 都有效,它们在 stripe 这个步骤都是一样的。在一个实测的案例上我们从 High 切换到了 Extreme,Wasm 的体积从 49.5MB 降到 44.8MB,global-metadata.dat 文件体积由 15.3MB 降至 13.3MB。
关于引擎的优化,除了像 Dotnet Wasm 这样直接提出全新方案,还有一种方式就是对更多的细节做极致的优化。比如接下来介绍的 IL2CPP 元数据精简。
IL2CPP 元数据就是 global-metadata.dat,里面保存的是 C# 类型、方法等信息,运行的时候会根据它拿到这些信息,然后执行方法。默认使用的数据结构可以支持超过 21 亿个方法或者类型,这个是从通用的角度来设计的,但是实际上在小游戏来说,方法数量可能就是万级别,用不着那么大,所以我们在打包小游戏的时候,现在会根据方法和数量自动选择满足当前要求的精简过的元数据的结构,可以把 global-metadata.dat 的体积缩小大概 15%。
内存分配器优化也是一个细节上的极致优化。引擎内存分配器,会记录每一次分配的信息,并且在这个层面引发多一次对齐,即使是在 release 版本也是如此。微信小游戏整体内存比较紧张,需要更极致的优化,所以去除了这个开销。还有内存 Alignment,引擎默认是 16,在微信小游戏上也不需要,我们优化成 4 个字节。实测效果,在一个中重度的游戏上面可以减少 10-12M 的内存占用。
团结引擎 1.1.0 版本加了 TextureManager 功能,主要解决两个问题,一个是之前的纹理在打包 AB 的时候只能是选择 ASTC 或者是 DXT 压缩格式,所以我们通过 TextureManager 功能来支持多纹理压缩格式,比如说打包一套 AB,然后就可以用多套的纹理压缩。
另外就是尽量去解决纹理的显存占用的问题。多纹理压缩格式支持这边,现在是可以自主选择不同的纹理格式,然后点下面的这个按纽,就可以生成多套的压缩数据。在运行的时候,比如说当前是 PC 平台,我们可以选择 DXT 格式,如果是手机,我们可以选择 ASTC 的格式,这样避免了纹理在运行时解压缩的过程,而且显存占用会更小,GPU 上使用的是压缩的纹理,而不是解压后的纹理。另外是我们可以根据设备当前的性能,来选择对应的压缩等级,比如说在低端机上,我们可以使用 ASTC8x8 或者 ASTC12x12,但是在高端机上,为了更好的画质表现,可以选择使用压缩比更低的 ASTC4x4 或 ASTC6x6。
显存的控制上,首先是支持了显存的预算控制,用户可以设置显存的最大上限,我们会对这个游戏当前使用的所有纹理做重要性排序,然后把重要性比较低的从 GPU 上剔除。
剔除之后的纹理,它如果被用到就不会在画面上绘制出来,这个上限是需要我们去设置一个比较合理的值,重要性的计算主要是通过以下四个方面,一个是纹理的上传模式,还有纹理的优先级,这两个是会在 Texture 的 Inspector 页面让用户自己去设置的,后面两个是可见性以及纹理的距离,是我们引擎自动做的。以往的根据分辨率去计算优先级,比如说 LOD 用的就是这种方式,但对 CPU 的消耗非常高,所以我们这里就是直接用一个相对的距离去做替代。
然后是纹理的生命周期管理,在以往的游戏的使用过程中会用 AssetbundleloadAssetAssetbundle.loadAsset()、SceneManager.LoadScene() 去加载资源,这个时候就会把用到的纹理加载进来,引擎就会去创建一个纹理对象,创建完之后立即从文件里面把纹理的数据读进内存,然后把内存里面的这个纹理数据上传到 GPU,纹理上传的过程是不管你这个纹理有没有被绘制的,只要这个纹理对象创建出来了,就会上传到 GPU 上去。然后我们在纹理卸载的时候也是同样的调用 AssetBundle.unload(),Resource.UnloadUnusedAssets(),这个时候需要把纹理的对象销毁掉,才会从 GPU 上把这个纹理的显存给释放掉,这是原有的逻辑。
在这样的逻辑下,我们需要把纹理的显存控制好的话,就要求比较高,首先打包 AB 的时候要做好分包,可能需要拆得比较细,或者需要一些第三方的资产管理插件来做。另外就是我们卸载这些资源的时候,需要知道什么时候可以卸载,所以需要通过引用计数或者一些资产管理系统来判断这个资源到底当前是不是在使用。
即使是这样做之后还是有一些问题,我们在调用 Resource.UnloadUnusedAssets() 卸载未使用的资源时,需要遍历所有的资源的引用关系,需要不少的时间。因为小游戏只有主线程,所以这个卡顿会非常明显。另外我们使用 AssetBundle.Unload(true) 接口去卸载所有资源时,需要等待 AB 内所有资源闲置。通常我们打包 AB 的时候很难做到每个 AB 里面就只有一个资源,这个时候如果 AB 里面如果有其他资源在用,就要等其他资源都释放了才能去调用。因为这些原因,游戏里面很多纹理虽然我们并没有引用或者使用,也很难去很完整地卸载干净。TextureManager 对纹理的显存做了管理,但是不改变纹理本身的生命周期。右边有一个图,纹理的四个状态,就是纹理的原本的生命周期。在使用 TextureManager 之后不会发生改变。在纹理创建的时候,首先对 GPU 上传这一步做一个延迟的加载,我们会在引擎底层自动去检测,当这个纹理真的被用了,才会上传 GPU 上去。比如说场景里面有一个非 Active 的 UI 或者是 Component 组件,挂上去的纹理就不会上传到 GPU 上去。另外一个是我们纹理用完了之后,没有被 UI 引用了,我们也会自动检测到你这个纹理当前不用了,这个时候就会把这个纹理标记成可卸载了。并没有马上卸载,是因为有些纹理可能是上一个帧不显示了,过个几帧可能又重新用,所以说我们是把这个标记成可卸载,在 GPU 显存达到我们之前设置的上限的时候,才会把这些纹理卸掉。这样 TextureManager 就能够解决我们在纹理显存方面的浪费问题。最后是纹理重映射,我们发现很多游戏里面打包 AB,它其实纹理是有冗余的。比如说你打了一个 Prefab A 的 AB,然后又打了一个 Prefab B 的 AB,两个 Prefab 使用了同一张纹理,但是纹理本身没有单独打 AB,这样的话这张纹理在两个 AB 面都存在,就会有一个冗余。AB 大小会变大,使用的时候如果同时加载,它就会在显存里面有两张一模一样的纹理。
另外是我们加载 AB 的时候,我们通过 AssetBundle.unload(false) 这种形式频繁反复加载,也会导致内存里面有多张完全相同的纹理。这个时候在 TextureManager 上传 GPU 之前会去检查这个纹理的二进制数据是不是一样的,如果一样我们就会做一个替换。当然我们也会排除一些情况,如果这个纹理是一个可读写的纹理,会跳过处理。因为可读写的纹理在内容改变后重新上传就把其他的纹理的数据写的不一样了,会出现一些问题。
接下来讲的是小游戏宿主方面,我们是在 Connect App 里面集成的小游戏宿主能力,小游戏宿主是基于 V8 开发的,现在能提供 CPU 使用率、整理内存以及日志时间的信息,然后我们打包的话也是比较方便的,通过引擎里面点一个构建并上传,然后直接就会生成一个二维码,二维码扫了之后,就可以跑起来,不需要其他的第三方的东西了。
最重要的两点是小游戏宿主会支持一个更高的 Profiling 时钟精度,我们在做一个小游戏的性能分析的时候,经常会因为像浏览器或者微信那边提供的一个时钟精度不够,大概是 0.1 毫秒的时钟精度,如果你这个方法的调用时间小于 0.1 毫秒,它可能就抓不到这个方法的调用时间,你就没办法知道这个方法的性能到底是怎么的。我们通过支持更高精度的时钟,就能够在更细的方法找到这些性能问题。另外是我们在小游戏宿主里面提供了 C# Debugging 的能力,后面会重点讲这个点。
打包到小游戏宿主的话是非常方便的,第一步从 package manager 里面去装一下小游戏宿主的 package,因为现在我们还没有把这个 package 放到引擎的安装包里面去,所以暂时只能通过这种 by name 的方式去安装,下一个版本应该会把这个功能直接加上,直接以搜索的方式去加。安装了 package 之后我们直接打开打包界面,在打包界面就可以把我们的游戏直接打包然后上传,通过 Connect 扫描下面的一个二维码,然后就可以把这个游戏跑起来,这个流程是非常简单的。
这里讲的是 C# Debugging,在其他平台,包括 PC、安卓、IOS 都是支持 C# Debugging 的,就是说 C# 里面代码有问题,可以去断点调试,看看变量的值,代码哪里出了问题。但是 Web 平台是一直不支持这个能力的,所以说调试起来比较麻烦,可能需要通过打 log 的方式去调,比较痛苦。
所以在这个版本中间我们就对 C# Debugging 的能力做了一个支持,原本 Web 不支持 Debugging 是有一些原因的,主要是下面这四点:第一点就是多线程在以前的 WebGL 里面是支持有限的,因为我们在调试的时候,我们需要跑两个线程,一个线程是游戏的主线程,另外一个线程就是我们的 Debug Agent,这个 Debug Agent 会通过 socket 去跟 IDE 建立一个连接,然后发送一些调试的指令,接收调试信息,但是在 WebGL 这边,因为 WebGL 是不支持 socket,只支持 Websocket,这是另外一个原因。另外还有两个原因,Websocket 跟 socket 是不一样的,它没办法做广播以及监听端口的一些操作。
因为这些原因我们之前没有支持 Debugging 能力。但是在微信小平台这边,我们首先是在这个平台里面支持了多线程。然后在小游戏宿主这边主要是通过一个中间代理,就是中间加了一个 Proxy,一边是通过 Socket 去跟 IDE 交互。我们会把 Socket 接收的信息转发到另外一个线程,这个线程是通过 WebSocket 发送消息,通过一个中间代理的方式,我们把 WebSocket 跟 Socket 桥接起来,这样小游戏的调试信息跟 IDE 这边就通了,可以进行代码调试。
这边是我们的使用流程,使用的时候,首先要在打包的时候把 Script Debugging 给勾上,在宿主这边我们需要去点一下打开 C# 代码调试,这两个操作完成了之后,我们把 IDE 和宿主运行在同一个局域网内,就可以连上了,进行代码调试。
这里是我之前做了一个案例的截图,可以看到左边这个图通过 Attach Unity Process 就能发现下面微信小游戏的调试进程,右边就是断点的状态,可以看到变量的信息,包括我们的堆栈都是能看到的。
接下来说一下 Remapper 的运行内存优化。Remapper 是我们在引擎里面一个保存序列化的位置和 InstanceIDID 之间的双向映射的结构,每个序列化文件会有一个 File ID,还有一个对象在序列化文件内部的 ID,这两个合起来就是序列化的位置。InstanceID 就是内存中的对象的一个 ID。在我们加载的资源非常多的时候,这个表就会很大,甚至极端情况下会涨到 40-50M,70-80M 都有可能。
优化前我们的 InstanceID 每次都是加或者减 2,InstanceID 是这样的,如果对象是序列化文件里面加载进来的话,它的 InstanceID 就是一个正数,如果是代码里面创建的对象,那就是负数了,但是不管是正数还是负数,每次去创建的一个新的 ID 它就是加 2,所以说 InstanceID 一直都是一个偶数。InstanceID 不会复用,每次新加了一个对象进来,它就会重新给它再分配一个 ID。我们之前的 Remapper 里面存储映射表的结构是一个哈希表,因为每次 InstanceID 增 2,内部是比较稀疏的。另外哈希表的内存增长策略是一个每次翻倍,所以导致有一些游戏在资源多的情况下,Remapper 内存会占非常大。
优化后 InstanceID 由每次加 2 改成了加 1,加 2 加 1 我们觉得本质上没有什么区别,所以说没必要通过加 2 的方式增长。我们也对 InstanceID 做了一个复用,主要是通过加了版本的概念,我们 InstanceID 其实没有完全把一个 32 bit 整型用完,我们可以在前面的几个 bit 加一个 version 的计数,这样 InstanceID 就可以复用起来,然后我们也通过一个比较紧凑的连续数组来替换哈希表,这样我们的内存每次就是 1024 个字节的线性增长。比如说在使用哈希表的时候,8MB 的内存已经满了,这个时候需要多几个字节,它马上就会涨到 16MB,使用连续数组的时候,只需要再分配 1KB 的内存就好了,这样就不会出现内存非常大的问题。Remapper 目前优化是只在微信小程序平台开始的,后面我们也会逐步向其他的,主要是移动平台上。
我们在团结以往的版本里面其实也支持了一部分的 SIMD,主要是在 Mesh Skinning 做了一个支持,效果也还可以。现在我们认为 Mesh Skinning 还是走 GPU Skinning 更好,所以说我们现在对底层的 Math 库,整做了 SIMD 的支持。因为 Math 库是给引擎各个模块提供基础能力的。为什么选 Math 库?因为 Math 库里面有非常多的矩阵和向量运算,非常适合这种 Wasm SIMD 的优化,我们支持的方式主要就是通过 WebAssebly SIMD intrinsics 去重写我们的 Math 库,这样就能让我们的引擎整个性能得到一个提升。
AssetBundle 的打包优化是一个比较小的点,但是也是开发者的一个痛点。以往打包 AssetBundle 非常慢,我们就针对这方面整个过了一遍 AssetBundle 打包的逻辑,改进了一下 AB 里面跟图集相关的逻辑,这块对 AB 的打包影响速度比较大。
改完之后,在一个实测的案例中,一共 2.5 万个 AB,大概加起来有 4 点几个 GB,打包时间是从优化前的 160 分钟减少到 70 分钟。顺便提到,虽然优化的是一个图集相关的逻辑,即使工程里面没有用到图集,但还是会有影响,所以说并不是工程里面用到了图集才会影响打包速度。当前这部分优化也是只在微信小游戏平台有,后面也会在其他平台逐渐开放。
最后讲一下微信 SDK 的深度集成,主要是有两点,一个是我们会在用户切微信小游戏平台的时候,自动安装微信小游戏的 SDK,这个 SDK 是我们在发布团结引擎的时候,当时支持的一个最匹配的版本,会默认给它内置进去,在你切换这个平台的时候会安装。第二个就是我们把微信的打包页面直接集成到引擎的界面里面来,这样就不用去来回切换页面,会比较方便一点。
在未来的团结引擎版本内,微信 SDK 的版本就可以自己选择,在游戏引擎里面就可以选择。现在大多数情况是下一个 package 然后导入进来,如果换一个版本的话也要重新去下,然后再导进来,操作比较麻烦;引擎支持的话整个流程就比较简单方便一些。
长按关注
第一时间了解 Unity 社区动向,学习开发技巧