微信公开课小游戏专场 | 游戏设计与性能优化分享
8 月 19 日,第九季微信公开课小游戏专场在广州举办,这也是本年度第一场线下微信公开课。基于 Cocos 引擎研发的《最强魔斗士》《英雄杀》《胡莱三国》《山海经异变》等热门游戏 CP 亮相公开课现场并带来了关于小游戏性能调优、广告变现及内购玩法的实战经验与“避坑指南”。其中,来自腾讯光子欢乐工作室的前端主程谭家章就《最强魔斗士》项目分享了小游戏性能优化经验,以帮助更多开发者制作出更加高品质的小游戏。
以下为直播演讲文字整理,字字干货,欢迎阅读!
大家好!我们今年上线了一款 2.5D 小游戏《最强魔斗士》,今天我主要分享一下这款游戏的技术设计以及性能优化经验。
先来简单介绍一下我们的游戏,《最强魔斗士》核心玩法是弹幕射击类,大地图采用斜 45 度视角的。今天主要做的是技术分享,所以我先说明一下我们的技术规格:
《最强魔斗士》是一款微信小游戏,使用的引擎是 Cocos Creator 3D。为了人物动作的流畅,游戏的主角和怪物使用了 3D 模型。此外,游戏采用固定视角,主要是便于 2D 跟 3D 相结合的表现。从性能角度考虑,其实我们更偏重于 2D 渲染,除了人物跟怪物以外,我们其他的元素比如地表、金币、子弹还有光效、阴影这些都是用 2D 渲染的。
技术方面有一点值得关注的就是,目前小游戏在苹果下面的性能瓶颈是很突出的,主要是 CPU 端性能。更具体地说,其实就是 JS 运算效率不足。另外,我们游戏本身也比较复杂,战斗逻辑复杂,同屏模型数量、2D 精灵数量都非常多,所以我们把支持的最低端设备的定为苹果 iPhone6s,后续提到的性能相关数据,也是基于这个设备来测量的。
今天我将分享三个方面的内容:
一、战斗方面的基础设计
战斗场景层级划分
人物和怪物模型的渲染
二、针对帧率以及帧率的稳定性所做的针对性优化方案
地表的实现和渲染优化
节点矩阵运算简化
节点增删性能的优化
三、瓶颈分析手段及后续思考
一、战斗方面的基础设计
1、战斗场景层级划分
先看一下我们的场景层级划分,层级划分主要有两个目标:一是更好地把 2D 跟 3D 元素相结合,二就是传统的 Drawcall 优化。
从图片中可以看到,我们把最顶层的节点划分为了远景层、近景城还有地表层,在地表层之上的又放着人物跟怪物的 3D 层,然后 3D 层的上下两边就各自放了两个动态的 2D 精灵层,动态的 2D 精灵层又细分为阴影、子弹、数字还有光效等等小层级。
划分之后,我们约束每个小层级只能用一张纹理图集。这样引擎在渲染小层级的时候,就可以动态地把里面所有的精灵合成一个 Drawcall 来渲染,就能够比较好地控制 Drawcall。
目前我们 Drawcall 控制在四十以下,算是一个比较小的值,不过 Drawcall 控制地小不意味着性能就很高,因为这只是其中的一个必要的优化。
2、人物和怪物模型的渲染
然后来看一下我们在 3D 模型渲染方面的一些设定:
我们的 3D 模型主要用在主角和怪物的骨骼动画,这里最关键的两项优化技术就是 GPU 蒙皮和 BakedAnimation。
GPU 蒙皮比较常用,它把复杂和耗时的顶点蒙皮从 CPU 转移到 GPU层;BakedAnimation 是动画系统的一种缓存策略,它把关键帧缓存起来以后,后面每帧只要去渲染就可以了,不用反复地计算。
这两项优化手段共同点都是以降低 CPU 运算为目标,而小游戏目前的瓶颈就在 CPU 这一侧,他们的优化效果是比较好的。
解决了单个骨骼动画的渲染问题后,还要处理当同屏数量很大的情况。
从图片中也可以看到,我们有些复杂场景是存在同时有数十个模型同屏的情况的,这个时候,就要启用 GPU Instancing 技术的优化了,它可以用一个 Drawcall 来渲染多个重复模型,这样就可以使得 CPU 端的 Drawcall 和消耗都控制得比较低,它不会随着模型数量的递增而线性的增长。
要说明的是,以上优化手段,其实 Cocos Creator 3D v1.1.0 以上版本都已经支持了。
另外,我们还关闭了实时阴影,采用 2D 精灵来模拟的方式。经过上述的一些设定之后,目前我们在战斗里面的模型渲染耗时大概是 7ms 左右,属于一个可接受的范围。
二、一些针对性的优化方案
1、地表的实现和渲染优化
然后来看下我们的地图,我们用小图块拼接的大地图,它的优势就是纹理复用,可以节省下载量和内存占用。
为了支持地图的快速编辑,我们开发了专门的编辑器,可以看到不管这个地图最终编辑成多大,最终输出的资源都是一张图集,这样可以比较好地控制我们 Drawcall 数量。
小图块拼接有什么问题呢?主要问题就是当图块数量很多的时候,渲染压力会非常大。在《最强魔斗士》中,把场景外的节点剔除以后,同屏图块数量还是达到 300 个以上,如果用常规的渲染方式去渲染的话,渲染耗时会达到 6ms 以上,这是无法接受的。
这么耗时的原因在于做渲染的时候要做动态合批,需要遍历所有的精灵节点,然后计算它们的顶点、UV、IBuff、Color 等数据,合成一个 batch 来提交渲染,这么做虽然 Drawcall 很低,但是 CPU 侧的运算量依然很大。
根据地图块固定不变的特点,我们联想到可以用静态合批的方式来优化。静态合批的优势其实就是把上述提到的动态合批的渲染过程,把它给缓存下来,然后后续每帧就不用去计算,只需要提交渲染就可以了,具体的实现引擎也有相关的组件可以比较好的支持。
经过了静态合批之后呢,我们的地表渲染耗时从 6ms 下降到 1ms 以下。
2、节点矩阵预运算简化
除了前面说的静态的地图块之外,我们场景里面还有大量的动态节点,比如说截图里面看到的一些子弹、光效、血条、阴影等等,它们的数量非常大,会有 100 个以上,每个 2D 精灵本身是处在一定的容器结构里面,所以场景里面实际上在运动的节点比我们看到的精灵数量还要多很多。
经过我们的统计发现,最复杂的关卡里面会同时存在 600 个以上的节点,节点数量这么大的会存在什么问题呢?最主要一点就是节点的矩阵运算会耗时比较多。
Cocos 引擎节点是通过矩阵来计算变换的,这也是一种最通用的实现方式。优势是可以完整地支持平移缩放还有旋转,劣势就是比较复杂,尤其是在 JS 上面去计算。
这么大量的矩阵运算有没有办法去优化呢?这里有一个关键点就是其实场景里面的大部分节点,并不需要支持旋转,而是仅仅需要做位移跟缩放就好了,旋转就是矩阵运算里面最复杂的一块。
所以呢,我们从这里就会看到一个可优化的点,我们的做法是这样子:基于前面的假设去修改了引擎在计算节点变换的逻辑,把计算位置这块的矩阵运算给去掉了,换成了一个简单的求和跟乘积运算,这样就能支持位移和缩放,也能算出节点的最终位置。
这项优化对于场景里面的 80% 节点都是有效的,所以带来效果也比较好,在复杂关卡里面的话可以有一到两毫秒的耗时下降。
3、节点增删性能的优化
前面提到节点数量很大,节点数量很大除了矩阵运算消耗以外呢,还有一个不太容易关注到的节点增删性能问题。
在 Cocos 节点树设计上,节点增删是有一定的性能消耗成本的,当同时增删的节点数很大的时候,这个成本就非常突出了。
《最强魔斗士》又是一款弹幕类游戏,节点数量增删特别频繁,比如说某些时刻我们会有几十个子弹创建,同时又有几十个子弹在消失,同时还伴随着数十个特效、阴影的创建和消失,统计发现复杂关卡里一帧最多会发生 200 次以上的节点增删。那么这个节点增删他带来的运算峰值就足以带来帧率的骤降,给用户带来卡顿的体验。
节点增删不是一种持续性的消耗,所以在平均帧率上面的体现不明显,所以因为这样而容易被忽略。
我们怎么优化这个问题呢?我们采用的方式就是懒增删的方式。就是一旦节点被添加到节点数之后,我们就不再移除它。当我们要隐藏的节点时,我们用一个轻量级的方法来替代引擎的接口,比如往节点里面设置一个隐藏属性,然后修改一下引擎的渲染流程。当遇到这些隐藏节点时候呢,我们就跳过他,这样就可以实现节点的隐藏。
这样修改以后呢,我们的节点增删就基本上没有多少消耗,也不会产生预算峰值和卡顿。
最后,分享一下我们的瓶颈分析手段以及一些未来思考。
首先性能优化,我们的重点对象其实是苹果设备,然后苹果设备上面的目前是没法做真机 Profile的,所以这正好给我们带来一些挑战。
苹果怎么做帧率优化呢?我这里推荐一个土方法吧,我们在引擎的主循环或者我们逻辑主入口里面去做一些耗时统计,这算得是个大循环的总耗时。然后运行过程中的我们动态去开关一些节点和功能,然后通过总耗时的变化发算出子系统的自身耗时。
这个方法的优势就是简单,它对于一些粗粒度的性能消耗统计是准确的,缺点也很明显就是他无法做更细粒度分析,比如说无法定位到具体函数。
另外也可以在 PC 和安卓上面做 Profile,有时候也可以在定位出一些运算热点。不过在 PC 上面做 Profile 的话,它反映出来的热点分布跟苹果上面是不完全一致的,所以这里要注意不要被误导。
这是我们做的一些工具:
左边的是节点管理工具,它可以在运行的时候展示整个场景当前的所有节点,然后可以去动态地禁用和激活节点,主要是用来评估某一类节点的渲染耗时,比如说骨骼动画的耗时。
右边是些 GM 工具,可以开关一些我们的业务逻辑,比如说把碰撞关闭、把怪物行为暂停。
经过上述的分析,我们可以得到整个游戏的一个耗时分布图。
可以看到,经过一轮优化之后呢,目前耗时大头就是模型渲染跟战斗逻辑这两块,总耗时达到了 25 毫秒,所以在苹果 6s 上面只能跑到四十帧左右。
后续如何进一步优化呢?我想除了需要有更好的方案以外,可能也需要苹果就是平台侧提供些更精细的优化工具,否则我们的瓶颈分析效率非常低。
后续还有比较值得关注的一点就是可以利用 Worker 线程来缓解主线程的压力,我们目前在做一个事情就是把我们的战斗逻辑给整体迁移到 Worker 去。
从图里可以看到,假设我们完成了这项迁移的话呢,我们的主线程消耗是可以大大降低的, 有28%左右。但这里还需要平衡的这个逻辑的迁移成本和 Worker 本身的通信成本。
以上就是我今天的分享。
谢谢大家!
《最强魔斗士》相关分享:
开发篇 | 腾讯光子 《最强魔斗士》3D 开发优化经验
策划篇 | 腾讯光子是如何制作《最强魔斗士》的?