游戏开发中的性能分析

在我国,移动游戏已经发展成为千亿规模的产业,近十年我国游戏产量呈井喷式增长。随着游戏行业的发展,游戏数量越来越多,玩家数量越来越多,面对诸多的游戏产品,玩家选择面也很多,于是玩家对游戏内容和质量提出了新的要求。
通常,一款游戏能否成功,能否留住玩家,最直接的因素就是美术、玩法、性能。美术直接关系到玩家会不会下载、会不会打开游戏试玩;玩法则是关系到你的游戏产品是否是“见光死”,玩家是否会静下心来的玩。性能则是关系到游戏体验,能不能因为游戏加载慢、卡顿、崩溃等问题直接让玩家扫兴。今天重点谈谈关于性能的问题。
作为一个游戏开发者,接触最多的就是游戏应用逻辑,游戏引擎层面的东西我们能做出的优化和魔改的地方很少,毕竟很少人有更好的解决方案来优化一款高度商业化的游戏引擎。所以我们能做的就是在应用层对游戏做优化,下面讲解一些相关的问题:

一、游戏资源

在2D游戏中游戏资源以图片、动画、字体为主,3D游戏资源以模型、贴图、光照数据以及UI用到的2D游戏资源。游戏中使用游戏资源可分为三个阶段:加载、使用/显示、卸载。

1.加载:

玩家下载一款手机游戏,玩家带着期望打开游戏。游戏通常在启动阶段会预加载绝大部分必要的游戏资源,这样做的好处是进入游戏后一旦需要加载资源,直接就在内存里读取流式资源数据,但是代价就是启动的时候变得缓慢,甚至卡顿。如果这时候手机加载资源黑屏几秒,那一定是很糟糕的事情。
绝大多数游戏产品在上线初期这种情况是屡见不鲜,解决这种现象的常用方法是设置一个启动屏,启动屏可以是一个单独的场景,也可以是一个单独的层,不过我建议使用单独的场景来做,这是因为这样的启动场景需要的资源少,加载快,这部分的资源在切换场景的时候通常可以直接释放掉。在加载进度的时候设置进度条实时反馈游戏正在处理的进度,甚至可以展示一些动画和游戏中需要了解的信息,有条件最好采用异步加载的方式,转移玩家的注意力,增强玩家的体验。
加载资源的顺序应该遵循先大后小,因为在引擎底层首先是将文件数据读进内存,然后是内存拷贝到我们的游戏资源缓存区,这会使内存在某一瞬间增大一倍

2.使用/显示

游戏资源通常会重复利用,例如一张游戏中金币的图标会在很多个界面出现,一个角色动画可以在多个场景里出现,当不需要展示这些内容的时候,尽量不要释放掉这些游戏对象,而是把这些资源用一个对象池储存起来,采用工厂模式进行存取,控制好资源对象的生命周期。
通常场景切换是无缝衔接的,这就要求下一个场景资源加载完成即将显示的时候再释放上一个场景的资源。这样就会出现一个问题,两个场景之间至少会有一帧是重叠的,那这一帧的内存就是两个场景的资源所占内存之和。也就是说场景切换可能瞬间让内存翻倍,硬件不好的手机可能直接就崩溃了。通常做法也是做一个过渡场景。原理如下图:
游戏开发中的性能分析_第1张图片
明显上图所示,采用过渡场景会大大减少内存开销。

3.卸载

游戏引擎具有自动管理生命周期和内存的设计,大大减少了开发难度,从而开发者不需要关心内存释放问题,但这不是绝对的,长时间的游戏进程也会出现脱离了引擎追踪的碎片式内存,所以有必要采用第三方工具来定时清理这些“不受控制”的内存片段,以coco2dx-lua为例,collectgarbage方法提供了定时清理内存的功能。卸载资源通常我们只卸载了游戏中引用的资源,仅仅这样是不够的,因为这些资源虽然没有被引用,但是依然储存在内存区,所以有必要将这段内存释放掉。

二、渲染

在3D游戏中,渲染过程的计算量更大,性能优化显得更为重要。以虚幻引擎为例,游戏帧率很低,甚至有卡顿的现象,可能原因是各种各样的,比如是不是人物太多了或者渲染的东西太多了,这样猜没有根据不能妄下结论,可能会浪费很多时间还找不到结果,当然如果也有可能很快就找到了但这不是解决问题的方法,这个时候我们可以借助第三方工具来查找性能问题。调试性能的第三方工具自己自行百度一下,包括调试CPU性能和GPU性能。
查找性能问题可以分别从CPU和GPU下手,启动你的非Debug版本游戏程序,在控制台输入Stat unit命令行,可以看到如下的显示:
游戏开发中的性能分析_第2张图片
这里解释一下,Frame显示从逻辑到渲染完成一帧所花的总时间。Game是逻辑线程的时间开销,Draw是渲染线程的时间开销,GPU是从这一帧开始到结束的时间开销。可以看到上图中逻辑线程所用时间占用了总时间的大部分,由此推断性能瓶颈出现在了逻辑线程上。
渲染线程上,适当设置场景中物体的LOD、可见距离,背面剔除,少使用半透明的对象都可以明显降低性能消耗。Shader里适当使用曲面细分,减少不必要的或者重复的像素渲染,关闭不必要的物体倒影,也能大大降低性能消耗。
在光照系统中,动态光照确实能获得更好的光照效果,这给硬件的性能带来巨大的压力。对于大场景中的光照效果,可以预先烘焙光照,将光照数据预存下来,使用的时候直接从文件读取。光照部分内容网上很多,在3D游戏中是很有必要考虑的内容。

三、算法

这里的算法主要是指在游戏逻辑中的算法,引擎层的算法不需要我们去修改,在游戏业务逻辑中选择高效的算法就已经足够支撑应用程序的性能了。

  1. 使用合适的排序算法。
  2. 使用合适的寻路算法。
  3. 避免使用除法。
  4. 避免使用开根号sqrt算法,尤其要避免开根号再倒数。
  5. 避免在update方法里频繁更新,特别是在渲染线程里,最好是监听到变化再更新。
  6. 代码中用到的容器首选高效的map,set,array。

    四、内存管理

    应用程序在启动的时候都应该申请一段内存空间,以供应用程序在整个程序进程中使用,如果内存变大超过了申请的内存空间,记得要主动申请更大的内存空间。为了避免应用程序出现崩溃,卡死等现象。处理内存泄漏,内存溢出,野指针,空指针/引用尤其重要。

  7. 内存泄漏:这里考虑需要手动管理的情况,在内存块使用结束的时候应该显示释放,以C++为例,类中的成员应该在析构函数中被显示释放内存(堆内存)
  8. 内存溢出:这种情况通常发生在递归函数的递归深度太大,或者递归没有结束条件,导致函数无限申请指令内存和占内存。应当避免这种情况发生,适当设置结束条件。如果正常申请内存出现溢出,这就是大问题了,可能是硬件内存确实太小,或者系统出了问题。
  9. 野指针:以C++为例,P指向一段内存A,在某个时候,你将P的指向修改为指向内存B。这时候A内存块并没有被释放,而且它所对应的指针也无从知晓,这段内存就成了一段无法控制的内存,如果多处出现这种情况,那你程序的内存就莫名其妙的就增大,性能就下降了。只有在重启应用或者重启系统的时候才能将这段内存释放掉。
  10. 空指针:当你使用某个对象或者每个指针的时候,尽量先做空判断,否则程序就会出错。当然如果你有信心保证它不会为空,也可以不做判断。

五、崩溃/卡顿/ANR/OOM/启动慢等信息收集

收集这些数据可以使用第三方SDK来完成,常用的有Testin云测,阿里的TokenData等等。这些SDK提供了相当完备的各种日志的收集,收集线上应用程序即时信息。
以上就是本人多年程序经验总结的程序优化和解决方案,分享参考,匆匆下笔草草写下,如有不足,欢迎指教。

作者:张鹏程
一个狂热的游戏程序开发者

你可能感兴趣的