《重返帝国》是一款高品质全 3D SLG 手机游戏,游戏场景规模宏大,玩家操作自由多变,画面上经常会出现超过 1000 个士兵一起战斗的场景。在有限的移动设备性能上,需要同时兼顾性能与品质,团队在尝试过 C#、C++ 以及 DOTS 等多种技术方案的选型与研究后,最终选择了 Unity DOTS。
Unity DOTS 对于腾讯天美团队可以说是一次敢为人先的选择。
《重返帝国》手游画面实录
 实战分享
团队与 Unity 中国保持密切的合作与交流,经过多次的技术迭代与优化,最终让《重返帝国》项目在移动设备上为玩家呈现了极高品质的视觉效果。
同时团队也积累了一套行之有效的方法论,以下总结了几点分享给大家。
1、Job 数据依赖分析与优化,提升整个系统的并发性;
2、将部分 ECS System 的 Job 与非 ECS 逻辑并行,充分发挥多核;
3、逻辑数据显示分离,提升 chunk 内存利用率,减少资源加载带来的卡顿;
4、针对 System 进行逻辑降频,保证效果同时也提升性能。
当完成整体的框架设计和核心的实现后,进行性能分析的时候发现 Job 的并发性并不高,且 worker 存在大量的 idle 状态,导致系统的整体耗时偏高。为此,团队专门开发了静态分析工具辅助找出 System 之间的读写冲突与依赖,通过数据拆分、数据备份来解决冲突,让耗时较高的 Job 能够并行。
数据依赖静态分析工具
数据拆分解决写入冲突
数据备份解决读写冲突
基于分析不断细化和调整,解决了数据的冲突依赖,显著提高了 Job 的并发性,最终达到了相对满意的并行效果。
数据依赖优化后 Job 的并发执行
之后,将 System 按照功能进一步的细化拆分,把一部分 Job 的执行提前到与非 ECS 代码逻辑并行,进一步从整体上提高了游戏帧率。
ECS Job 与非 ECS 逻辑并行
在进行了 Job 并行性能优化之后,团队发现在大地图上拖动时存在由于 Entity 资源同步加载导致的一些耗时峰刺,这将对玩家体验造成瑕疵。所以针对 Entity,团队选择将逻辑与显示分离,一方面让资源可以异步加载减少卡顿,另一方面也提升了单个 chunk 的内存利用率减少 CPU 的 cache missing。
逻辑显示分离-资源异步加载
最后,在不影响效果的前提下,针对部分 System 进行逻辑降频与错帧(如移动逻辑计算相关的 System 降到 12 帧、耗时较高的 MoveJob 与 AnimatorJob 错帧执行),让整体的耗时更加平滑,并且有效的降低了游戏的功耗。
 性能优化
为了能更顺利地将 DOTS 适配到项目中,在原始框架的基础上,团队也在资产与渲染方面也进行了大量的按需开发。
在接入 DOTS 技术栈时,主要面临了以下 3 个问题:
1、资源兼容性:因为在接入时已经处于项目中期,很多游戏资产及对应的生产工作流已经成型,所以如何将已有游戏资产转变成可在 DOTS 技术栈中运行的资产,是需要解决的问题。
2、逻辑阶段的基础开销过大:可能会导致千人同屏场景出现时出现卡顿。
3、渲染阶段无法修改自定义的材质属性:因为对于战斗场景的还原重度依赖 GPU Instancing 技术,所以需要很多自定义的材质属性可以在运行时被复写。
于是,针对以上问题逐个研究核心痛点,团队找到了适合的解决方案。
在资源兼容性方面,在综合评估了各种方案之后,决定实现一套自己的序列化和反序列化流程。分为离线和运行时两个阶段:
离线时,将游戏中各类资产对应的 prefab 拆分成二进制文件和引用到的资源文件;
运行时,创建了一个“deserialize world”,用来把离线时生成的二进制文件和资源文件反序列化,生成 entity。当 entity 生成好后,再把它们移入 default world 进行运行。这样既可以在资产制作阶段使用熟悉的 prefab,也可以减少运行时的转换时间。
资产 Entity 实例化
HybridRender V1 在逻辑阶段开销过大,问题核心是主线程阻塞。比如整个生成合批信息的过程都是放在主线程中进行的,这个过程有很大的优化空间。
优化方向就是多线程化,充分利用移动端的多核优势。其实在生成合批信息时,不同的 RenderMesh 一定对应不同的 batch,任务本身具有可多线程化的特性。所以如下图所示,团队分配了一个较大的缓存数组,数组的大小与线程数量和 RenderMesh 数量相关。多个线程并行完成对含有 RenderMesh 的 Chunk 进行筛选,并填入缓存数组的指定位置。因为在缓存数组中,每个线程都有自己的写入空间,所以多线程并行时,不会产生数据写入冲突。
多线程 RenderMesh Batch
团队还对游戏中 LOD 的结构进行了优化。游戏中的模型一般有 4 层 LOD,在转换成 entity 后,将会有 6 个相关的 entities 生成。
过多的 entity 不仅浪费内存,同时也会导致很多冗余计算,而根据 LOD 的特点,可以只记录单个 LOD 的信息,在渲染时按需替换成应当显示的 LOD Mesh 即可,这样就可以把原本的 4 个 LOD 网格当做一个单独的网格来对待。同时,也将 LOD Group 节点和 Root 节点进行了合并,Entity 的数量也从原来的 6 个下降到 2 个,性能也有了提升。
这种方式带来的一个额外好处是当更高层级的 LOD 还未加载完成或渲染压力过大时,可以只加载低层级的 LOD 模型来显示。
LOD 结构优化
为了在 C# 中更改材质的 Instance 属性,团队定义一个和 Instance 属性完全匹配的 IComponentData Struct,在数据对齐方面,遵循 std140 内存数据对齐原则。如下图所示:
Instance 属性对齐
在渲染运行时,根据 entities 的数量预先分配一块大的缓存,之后利用多线程把各个可见的 entity 的 InstanceParam 数据复制到 Buffer 中的指定位置。最后将整个缓存直接提交至 GPU,就可以按照传统的 GPU Instance 方式来使用缓存中的数据了。
在有了 RenderMesh 上的材质信息和 mesh 数据之后,的 InstanceBuffer 也组织好了,这样通过调用 Unity 的 DrawMeshInstanced 接口就可以进行渲染了。
Instance Data Buffer
以上都是团队在实践中不断迭代总结出来的宝贵经验,希望能对那些同样想使用 Unity DOTS 技术的团队能有所启发。
演讲人:
肖健
客户端组主程序
腾讯天美 T2 工作室
肖健于 2014 年加入腾讯天美工作室群,目前担任《重返帝国》客户端组主程序。肖健在游戏框架、Gameplay 和性能优化等方面都有着丰富的研发经验。
侯仓健
引擎负责人
腾讯天美 T2 工作室
侯仓健于 2014 年加入腾讯游戏,曾参与多款手游的研究与开发工作。侯仓健于 2019 年加入腾讯天美工作室群《重返帝国》团队,目前主要负责游戏引擎和工具链的开发工作。
长按关注
Unity 官方微信
第一时间了解Unity引擎动向,学习最新开发技巧
 每一个“在看”,都是我们前进的动力 
继续阅读
阅读原文