引言

几周前,我开始写一款叫Sky Blocks的游戏,使用Unity引擎并且发布在了安卓手机上,如果你有时间可以在Google Play上下载体验一下。

在写这个游戏的过程中,我遇到的问题大部分都是性能方面的。下面我来介绍一下这款游戏,以及性能问题的解决方案。

这款游戏混合了《俄罗斯方块》和《太空入侵者》这两个游戏的玩法。玩家将方块尽量的摆成一条线,方块会从下往上运动,最后停在屏幕的最顶端,但是不会想《俄罗斯方块》那样连成线以后就立即消失。你有60秒的时间摆出尽量多的线,时间一到就会有UFO入侵你完成的防御工事,一旦它们将你的防御都摧毁以后就会攻打地球,当地球的生命值为零游戏就结束了。

这游戏听上去挺简单的,但要做好还得下些功夫,不过,这样才有意思,非常有意思!

写代码前一定要计划好

写代码之前一定要计划好,这非常重要。我在开始开发Sky Blocks之前就没有想好要做什么,没有考虑清楚游戏的玩法。在正式开发Sky Blocks之前我走了一些冤枉路,我用JavaScript和HTML5写了一个《俄罗斯方块》游戏,后来又用C#将其移植到Unity,但是在移植的过程中没有考虑二维和三维的区别,仅仅是复制粘贴的做法,遇到碰撞检测的bug也只是做了小幅的调整。

因为没有简单的在每次刷新时更新整个游戏面板,所以我将整个面板都布满一个最简单的方块,这些方块用来作为背景网格定位。每次网格刷新时都会将所有方块销毁然后重新实例化,对于我来说,这样做已经很不错了,在电脑上运行的很流畅。

一般,一个面板上会有20行10列的网格,有可能会不断地有200个背景方块需要渲染、销毁、重建。另外,还会有不断增加的防御线(10个方块每行),每个方块都会有自己的材料,每个材料都会绘制。假如,一个游戏面板上有150-200个方块需要渲染,那么,就会调用大约200次的绘制函数。

如果我在动手之前有明确的计划,我就会发现这么做游戏是不会运行太久,有可能就会在一开始就省去我大部分时间。

解决

在动手之前画一个草图是一个好主意,这样就可以通过整体分析找到哪些内容是不变的?这些内容有什么不同?在Sky Blocks这游戏中是游戏面板、防御线和UFO。

游戏面板用来控制游戏,包括可移动的方块和已经锁定住的静态方块。

防御线由10个方块组成并排在一条线上。UFO都是由网格组成,它们可以在防御线上面移动。抓住这几点,我就接近成功了!

减少绘制函数调用

我之前也提到过,我的实现方式会大量调用绘制函数。当在安卓设备(三星galaxy S4)上运行游戏,随着游戏内容越来多,我发现游戏变得越来越卡了。为了让游戏运行流畅,我首先考虑减少绘制函数的调用次数。我在网上查相关资料,绘制函数会消耗多少资源?怎么造成的?该怎么优化?意识到效率问题,我不能出一个好的测试方案,找到一些测例会对CPU和GPU造成严重的压力就行。一些测例没有太大的影响,但有些则会让我的帧率变得很低。

影响效率的问题还是比较容易发现,其根源就是不同的方块有不同的材质。

要减少绘制函数的调用就得减少object的数量和材质数量。

因此,我写了一段代码用来生成一个网格,这个网格就可以替代原来有200个立方体组成的游戏面板。然后,我用顶点着色的方法替代贴图方法。最后,我将材质的着色器换成在网上找到的一个不发光的顶点着色。现在我将所有重复的绘制函数调用都减少为只调用一次!

对于防御线的处理,我用了类似的方法。我将所有材质都替换成了不发光的顶点着色,我没将它们用一个网格代替,而是保留了10个立方体,因为,替换了材质就已经不再多次调用绘制函数了。

非常遗憾,我事先没有准备,所以这里没有提供优化前后的区别对比图片。下面是一个已经优化好的游戏截图:
 


上图目前这个方块只调用了一次绘制函数(Batches:20、SetPass calls:20),之前提到每个方块由4-5个立方体组成,这个方块就处理了4次。
 


现在,已经放置了两个方块,但处理次数只增加了一次。若是之前的版本,每激活一个方块中的一个立方体都会处理一次。
 

 


防御线也是同一种模式,但实际上有10个四边形,它们采用顶点着色法并且共享材质。这个例子中,我们没必要将防御线做成一个网格来绘制,因为,Unity会通过“Saved by batching”来自动处理这个问题。

UFO比较棘手。每一个UFO都由上中下三块网格组成,因为,我想让UFO的外观是随机组成的。UFO的每一部分包含3-4个材质,手机号码购买那么一个UFO很有可能就会调用12-17次绘制函数,而实际上会调用17-30次。会有2-3个UFO同时出现在屏幕上,那么就会调用50-100次绘制函数,该死!

 


对于这点,我迫切的想要减少绘制函数的调用次数,所以我在网上找到了一个脚本,它可以将这些网格合并为一个。但是这个脚本不可以处理材质的问题,于是我最终将UFO都着成一个颜色,这样UFO变得非常丑。我手动改了一下那个脚本,现在UFO可以绘制最多2种颜色了。来看看行得通吗?现在只调用了30次绘制函数,但是我也不确定运行效率是否真的有所提升,不过至少我将每个UFO绘制函数的调用次数减少到了10次以下。
 


减少绘制函数的调用次数是不是适合所有的情况呢?当然不是。如果你可以减少当然最好,但是对于有些非常棘手的部分,只有通过牺牲显示效果来提升效率。

现在,对于绘制函数调用次数的优化,我已经从150-200次减少到了75-90次,还是很不错的!

最后,轮到UFO发射的激光,它们也都是单独的材质,当UFO进入狂暴状态时,绘制函数需要调用30-40次。非常幸运,这次只需要建立一个原始的四边形,它使用不发光的顶点着色法,这样,所有的激光只需要调用一次绘制函数,即使是UFO都进入了狂暴状态。

现在,游戏中所有的绘制函数减少到了30-45次,不错不错!剩下的就是UI中的绘制函数了,但是UI的处理我没有找到好的办法,不过我还是非常满意现在的效率了,现在游戏比之前运行得流畅许多。
 

 


UFO还是占了一部分绘制函数,但是考虑到已经从30减少到了不到10次,还是有很大进步。

一个首要的原则就是使用尽量少的材质,如果可以,最好共享材质而不是每一个都实例化,这样,会减少很多绘制函数调用次数。

资源只加载一次

在我的一些代码中,我使用了一些Resource.Load函数,但是由于Unity不会将已经加载的资源缓存下了,所以游戏运行中就会反复加载资源,这样非常消耗效率。我之前就会把激光的材质反复加载,这样虽然不会在PC上有什么影响,但是在安卓设备上就不行了。

为了避免反复加载游戏资源,我建立了一个静态的Dictionary(资源名称为Key,资源为value),当我要用某个资源的时候我会先去查询字典中是否有对应的Key,若是没有才会加载资源,反之,我就直接用缓存的资源。

避免多次调用Instantiate函数

我没有考虑过GameObject.Instantiate函数的开销,在代码中大量的使用了它,我本以为这个函数的开销也就和new一个类对象一样,但是,我错了!在GameObject的新建与销毁时都比较消耗CPU。然而,我在实现UFO攻击的时候就遇到了这个问题,每当UFO开火时都会新建一个激光束,然后再非常短的时间内又将其销毁,这样,在短短1.5秒内就有可能调用20-40次新建与销毁函数。太棒了!我终于找到UFO攻击时性能不高的原因了。

解决这个问题的方法就是做一个对象池。我构建了一个空的对象池,当我需要新建激光束的时候,我会先去检查对象池中是否已经存在,如果存在,改变一下它的位置与血量就可以直接用了。

如果对象池中没有我想要的,我就会像之前一样新建一个。当在需要销毁这个对象时,我并没有直接销毁,而是又将其交还给对象池,然后将其设置为非激活状态。通过对象池的方法,在UFO攻击的时候CPU的开销降低了25-30%!

总结

绘制函数开销非常大,你应该尽量减少调用次数。减少材质数量可以非常有效的减少绘制函数调用。少用不同的贴图,可以将多张贴图烘培在一个图集里面。如果时2D游戏就少用光照,甚至可以像我这样做,不要光照只用色彩描述就可以了。着色器不要带有参数,而是用不发光的顶点着色,我这个项目中就是这样做的。以上这些可以大大减少绘制函数的调用次数。如果你觉得这样视觉效果达不到你的要求,你可以试试增加一些网格。

Instantiate函数非常慢,所以尽量少用它。在游戏一开始你可以将大部分你经常需要用到的对象构建好放在对象池中,要用的时候只需要将其激活。



绘制函数和Instantiate函数并不是唯一优化对象,绘制函数销耗CPU和GPU,Instantiate函数消耗CPU。

如果你有一个非常精细的模型或者一个过程非常耗时,这时减少绘制函数并不能起到什么作用。优化CPU的消耗时重中之重,你可以梳理你的代码,看看是否有大量重复执行的内容,然后想办法优化它们。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐