« 有序数列的数据结构优化 | 返回首页 | 疑似 covid-19 二次感染 »

手机游戏引擎的优化

我们的手机游戏引擎一直在跟随着游戏项目的进程不断优化。一开始是因为游戏引擎在手机上帧数达不到要求。得益于 ECS 框架,我们把初期用 Lua 快速开发出来的几个核心 system 用 C 重写后,得到了质的飞跃。

其实这些核心代码总量并不算大。例如在 profile 中表现出来的非常消耗 CPU 的一个场景树更新系统,用 C 重写了也才 200 行代码 ,但在优化前 Lua 版本会消耗超过 1ms 的时间,而用 C 重写后,时间已可以忽略不计。

另外,我们采用了类似 skynet 的 ltask 做多线程框架,把业务尽量拆分到多线程中并行处理,这也极大的减少了每帧的耗时。除了主业务逻辑外,UI 、粒子系统、IO 被分为几个并行线程。且渲染底层的 bgfx 也是按多线程渲染设计的。这些并行流程间只通过少量的消息通讯,所以,并行的总工作量并没有比单线程模型更多。ltask 也可以很方便的调节工作线程的个数,用来更好的适配手机的 CPU 。

从xcode 的调试信息看,在游戏场景丰富时,大约会占用 280% 的 cpu 。换句话说,如果我们采用的是单线程架构,在不删减特性的前提下,做到流畅是相当困难的。

我的开发用手机为 iPhone 12 mini 。目前,游戏锁定在 30fps ,而每帧实际时间开销为平均 10ms 左右 (7ms ~ 15 ms 之间)。所以理论上锁定 60fps 也是完全做得到的,但因为不是动作类游戏,所以无此必要。

目前遇到最大的问题是游戏的能耗。优化游戏引擎让它尽量减少能耗,是过去在 PC 上做开发所没有过的经历。

让玩家的续航时间更长是一方面的原因,更重要的原因是:一旦让手机长时间工作在高能耗状态下,超过了手机的散热设计功耗 (TDP),必然会逐步发烫。最终会导致手机 CPU 自己降频,帧率也达不到了。

以前以帧率为唯一衡量标准放到手机上就行不通了。如果只为帧率达标的话,一般是在达标的基础上尽可能的提升画面质量,或是采用更简单易维护的算法。例如,有些算法为了简单,就直接每帧重复计算。而 cache 计算结果尽量避免重复计算,通常会增加代码的复杂度,却能减少 CPU 的使用。

现在,优化变得没有上限。只要能减少 CPU 开销都值得做。减少的开销全部能兑换成更长的续航时间和更高的散热效率。从散热角度看,手机真不是个好的游戏设备,switch 这种带风扇的设备要好得多。GPU 的开销更是如此,虽然 iphone 的旗舰级的 GPU 性能开起来远超 switch (从字面上的数字看,超过了 ps4 ),但实际上峰值性能除了给人几秒的惊艳外,远远达不到玩家的需求。iphone 12 只有 6W 的 TDP ,而 switch 的 TDP 达到了 15 W ,是它的两倍半。我们在手机上设计游戏,需要适当裁剪效果,把能耗控制下来才行。

我们的引擎使用了基于物理的渲染 ( PBR )。材质、光照都比较复杂。技术虽然不算新,但现在看起来在手机上运行还是比较勉强的。我发现像素着色器(fragment shader)开销比较大。而这恰巧是最容易优化的:改一行设置,直接降分辨率即可。iphone 12 mini 的视网膜屏有 476 ppi ,如果按最高分辨率的像素绘制 18x6 的点阵字母会小到我的眼睛几乎分辨不清。按一半分辨率渲染 1170 x 560 给我的感受,画面质量只下降了一丁点。但能耗居然可以下降到全分辨率的一半左右。(而每帧的时间开销并无太大区别)

为了得到准确的能耗情况,我没有采纳 xcode 给我的数据,而使用了更苛刻的测试方法:

每次测试前把电量充到 97% ,然后静置到刚好变成 96% 的那一刻开始启动游戏,加载足够复杂的场景,连续玩 16 分钟。以这样相同的测试条件,分别对全分辨率和半分辨率各测试两次(共四次),我发现全分辨率下的耗电几乎是半分辨率的一倍。

而考虑到能耗开销由 CPU 和 GPU 共同承担,修改分辨率丝毫不影响 CPU 的使用,那么 GPU 开销实际不只翻倍。或者说,我们引擎的 CPU 部分优化已经足够了,接下来需要重点考虑的是 GPU 开销。

btw, 我们游戏引擎几乎是用 Lua 编写,只在最近一年把 profile 后发现的少量性能瓶颈系统平替为 C/C++ 版本。用占比不大的 C/C++ 代码解决了 Lua 框架的性能问题。而 Lua 部分易于开发维护,(多虚拟机)还可以更好的适应多线程框架。


另一个能耗大头是特效系统,它主要是 CPU 方面的能耗很大。这块使用的 effekseer ,美术一开始只想追求好的效果,使用的比较粗犷。一开始发现它吃 CPU 比较多后,就将其移到了独立服务(可并行的线程)中,每帧耗时(实际帧率)倒是立竿见影的降低下来了,但能耗并无改善。毕竟,做的工作并无减少。

我想到的折衷方案是在镜头之外的粒子发射器就停止工作。这样从画面上来说是不准确的。设想一下一个火焰或烟雾效果,粒子片是逐步弥散开的。如果粒子发射器一旦离开镜头就删掉,重新进入镜头再重新发射,画面上会有些奇怪。但这样的画面 “bug” 我认为是为了性能而必要牺牲的准确性,可以接受。


我觉得只优化引擎代码是不够的。接下来的时间,还需要仔细优化美术资源:尺寸更小的贴图,更低面的模型更简单的动画,应该可以带来更好的性能。

Comments

有个的思路,可以兼顾粒子的体验和性能

为粒子设置个大体包围盒,设计阶段就可以确认,可以适当小一点。
这个包围盒离开屏幕时,把粒子系统停了并且从渲染树移除,但不清理
相当于停留在最后一帧,直到下次包围盒跟屏幕相交,再继续激活播放。

PBR渲染需要的贴图类别很多,如果尺寸能降低一半,那么优化的效果是立竿见影的,mesh面数的优化远不如贴图数量&尺寸的降低来的效果明显,这个工作难度不大, 但是很难推进,呵呵~

"如果粒子发射器一旦离开镜头就删掉,重新进入镜头再重新发射,画面上会有些奇怪。"
Unity解决这个问题的方法是,重新激活时通过 warm up 快速跳过一段时间,大部分特效的粒子行为都是可以预测的,可以直接算出一段时间后的状态,不需要模拟每一帧,只有少量基于物理的特效不能预测。你们有计划加入类似的功能吗?就不用牺牲的准确性了。

https://developer.arm.com/documentation/102697/0103/Mali-G76-GPU-performance-counters?lang=en

https://www.bilibili.com/read/cv9070200

https://www.bilibili.com/read/cv9660378

如果分辨率降低一半那么图像中绘制的像素就只有原有的1/4,理论上消耗的计算量应该是1/4左右,如果考虑到一些其他方面的消耗,大概应该也会在1/3上下

看到“全分辨率下的耗电几乎是半分辨率的一倍”我还以为完蛋了竟然没变化,还是CPU bound。结合下面发现完全不是CPU bound呀哈哈。

另外有一个小点我在硬件那边看到过,说是即使CPU达到目标帧率,还是要继续在并行度上优化,尽量将任务分配到所有核上。总计算量不变,多核并行可以允许CPU运行在更低的频率更低的电压上,能更节能。不过我自己没这么搞过,不知道到底有没有实际收益啊。

印象中显存传输带宽也挺耗电的,降半分辨率减少的功耗不只是减少了gpu像素计算,也有带宽减少的效果

Post a comment

非这个主题相关的留言请到:留言本