悠米高级技术总监韩天扬:UE4手游如何进行性能优化?

【GameLook专稿,转载请注明出处】

GameLook报道/2020年11月27日-28日,在虚幻引擎技术开放日Unreal Open Day大会上,悠米互娱的高级技术总监韩天扬做了关于UE4手游性能优化的专题分享,从公司自己制作的两款游戏出发,向大家讲述了在制作过程中所面临的优化问题,以及关于优化的几点思考。

以下是GameLook整理的刘伟演讲实录:

大家好,我是悠米互娱的高级技术总监韩天扬,那么非常荣幸能有这么一个机会跟大家进行分享和交流。我们的公司成立于2016年是一家创业公司,创业的主力人员都来自金山和畅游,都有10年以上的游戏开发经验,我们公司从成立之初就确立了开发精品游戏作为我们的开发方向,那么我们认为后续手机游戏就是说次时代的表现效果将会成为手机游戏的这么一个趋势,所以我们就选择了UE4引擎。

我们这几年开发了两款手机游戏,其中一款是《天空之门》,另一款是《救赎之地》。其中的《天空之门》是一款开放视角的MMORPG,《救赎之地》是一款采用了全场景PBR工艺的俯视角的竞技类游戏,这款游戏预计最近也就要上线了。《天空之门》就是已经在之前就上线了,在开发这两款游戏的过程中,其实我们也使用了一些平常之前在手机游戏上不太敢用的一些特性和效果,也给我们的这个优化带来了一种很大的压力,今天就是想跟大家分享的这个主题也是和咱们UE4的手游开发这个性能优化相关的一些主题。本身由于我们开发的两款游戏其实用的也是不同的手游版本,不同的UE4的版本,总结的这一些东西可能在后续的这些实际应用中可能会有一些差异,这块如果有一些什么样的错误,也欢迎大家进行指正。

为什么我们特别地重视手游的性能的优化,就是因为首先在于我们的国内的硬件设备。国内的安卓市场,它长期存在高中低设备共存,在安卓平台上低端机的比例又很大,拿今年的机型举例子,你像今年20年的话。本身骁龙865已经发布了很长时间了,但是在市面上你仍然能看见骁龙435,手机的处理器的时间跨度也比较大,435是属于16年的,我们现在仍然能看到,它们从GPU上的性能可能是相差了有几十倍的,为了能给玩家一个好的体验,我们就必须对这些低端机也进行一些优化。那么同样你像iOS这块的话,它的设备类型相对就比较简单,而且硬件性能也比较强。我们今天交流的主要内容还是针对这个安卓平台的。

刚才已经提到了我们面对的游戏类型,像MMORPG还有俯视角竞技类游戏,那么MMORPG它本身,就是需要开放视角,需要能看到很远的这些场景,还能看到大量的角色,每个角色分成多个部件包括武器之类的,竞技类游戏它需要高帧率、不卡顿 帧率还要非常平稳,达到后期的时候可能10~20人在一个区域里面战斗,特点就是人多、UI多、特效也多,我们在优化面对的问题可以分为以下那么几个维度,一个是内存,一个是帧率,一个卡顿,还有一个游戏效果,那么后续的分享也会从这几个维度逐一展开讨论。

关于优化的思路首先就是我们要对我们面对的目标机型进行分档,我们面对的目标机型是什么样的,每一档我们关注点是什么,要达到什么样的指标,这个就涉及到一个量化,每一档我们的性能指标是什么,确定了这些以后我们就开始对项目进行优化,并且进行迭代。

举个例子,那么比如说我们对那个《救赎之地》它的这个高中低机型的分档是这样的,高端机是从骁龙845开始,骁龙845及以上的,中端机从骁龙660开始及以上的,低端机是骁龙435 为一个我们的样板机。你会发现就是整个的这几个机型,它的上市年份从16年到18年的都有,16年的机器和18年的机器它显然不是一个档次的,上市的价格也是从800块钱到2000多块钱都有,这也说明了本身我们的这个国内安卓机分配市场的复杂性。

优化的思路就是首先就要进行匹配分档,在UE4引擎里面提供一个比较方便的就是Base Device Profiles.ini,在里面可以配置Matching Rules,那么我们在Matching Rules里面就可以把各种机型根据GPU或者根据手机的型号,进行一些分档,把这些具体的每一档次的信息在Default Device Profiles.ini里面进行控制。比如说我们可以去根据不同的这个档次去控制它的阴影,控制它的Bloom Quality,比如说Mobile Content Scale Factor之类的这些东西。

首先作为优化,我们需要首先就是要定位这个整个的性能瓶颈,性能瓶颈到底出现在哪,比如说它的帧率比较低,它到底是卡在CPU上还是卡在GPU上,我们最简单的方式就是可以通过这个stat fps和stat unit 获得这些东西的信息,那么在运行时我们就可以看到,跑的这个帧率和各个主要的这些线程的这么一个关系.如果这个我们从那个stat unit上可以看出来,像我们一个Frame他花了多少时间,我们再看Game、Draw和GPU大概花了多长时间,大概就可以去定位它到底是卡在什么地方,因为这个Frame它代表了这一帧的时间,这一帧的时间 主要就是和Game、Draw以及GPU 它里面哪一个时间花的最长,和它的时间是相匹配的。

我们在使用的这个优化时候用到的比较多的工具,在CPU方面用到的是UE4的前端工具,GPU方面我们用的比较多的就是高通的Pro filer,那么CPU的瓶颈 常见就在Game线程上,通常可以分成这么几个点,第一就是Tick,第二就是蓝图中的大量的广播事件,然还有就是蓝图中的大量的使用循环、数学运算这些东西,还有就是引擎的Character Movement,另外还有UI Slate Prepass音效并发,Skeleton Mesh的更新,以上这几个都是我们在项目中实际遇到的问题,后续会给大家逐一讲解。

1.Tick。这个本身它是必不可少的一个东西,我们能做其实就是减少Tick的频率并且合理设置各个Actor和各个组件的Tick interval,Interval就是每一个Tick的间隔。举个例子我们默认每一个角色其实都是按照一定的固定的Tick在跑,我们面对的角色比如说主角还有一些其他的怪,这些东西有一些可能离我们非常远的,这些离我们比较远的这一些怪物或者人物,我们其实就是可以调节它的Tick的频率,让它以一个比较低的Tick频率去运行从而减少一部分的CPU开销。对不敏感的逻辑可以进行这些分帧的计算,举个例子比如说我们游戏里面UI上有一些红点提示,这些东西它对于这个整个的要求 不要求那么实时,但它的一个计算量很大,本来我们如果把它放在一帧里面去做,那可能这一帧的开销就会比较大,如果我们把它分散到10帧里面去做,主观感受上不会有太大的差异,但是CPU会比较省。

我们还有一些功能开销比较大的,比如说我们的游戏里面使用到的那个一些债务的和可见性不可见的计算,这些东西我们都使用多线程去分担Game的压力,我们经常会将蓝图中特别耗时的计算的移到C++函数里面,我们发现就是蓝图里面处理一些纯逻辑方面的东西,包括一些数学运算什么的,它本身比起C++来讲的话,它还是稍微要慢一些的,把这些东西尤其是大量的数学计算从蓝图里面移到C++里面去会节省出可观的CPU时间。

2.蓝图中大量的广播事件响应。那么对于广播事件也是在大规模的应用的时候 需要非常小心的一个事情,我们要控制广播的影响,广播消息的影响范围。举个例子,我们游戏里面每一个角色其实它都有一个名字板,上面显示了一些血条一些其他的信息,如果在少的时候看不出来,但如果在非常多的时候,比如说我同时有几十只怪和这个人在那里战斗的时候,你就会发现这些广播消息它响应的比较频繁。我们的血量变化这个东西可能会同步给所有的人,但每一个人其实他只关心的又是自己的,于是它的名字板蓝图的事件响应里面判断了一堆东西之后,最后看这不是我的,只有一个人响应了这个消息,但是我们可能有四五十个人,但是只有一个人响应这个消息,这个时候其实是比较亏的。

我们其实完全可以在C++里面进行判断,在蓝图里面每一个名字板 ,只想要自己的这个消息就可以了,不要去扩大消息的广播,还有就是控制广播消息的频度,还是拿刚才的例子说,那如果我接受到一个HP变动这个消息,那血量变动其实我有时候一帧里面会接收到很多次,我在和一些怪物去战斗的时候,那可能同时和几十只怪,在战斗的时候它的血量变化是非常频繁的,但对于玩家的感官上来看,他其实注意不到就是一帧中的多次变化,所以我们可以把它合并一下每一帧处理一次,感受上差不多。还有一个就是控制这些就是响应消息的处理的复杂度,如果有太复杂的东西尽量就是别在蓝图里面处理。

3.蓝图中大量的使用循环。数学运算、查找等这些密集计算的操作这些也都比较费,尽量的这些东西不要在Tick中进行循环。复杂的逻辑还是要把它从我们的蓝图里面挪出去,蓝图里面尽量地只控制处理的流程。

4.Character Movement。我们在大量的玩家移动的时候,发现这些玩家移动的Movement的组件这个Tick的耗时很高,后来查了一下代码,本质上除了Movement它自己的Tick消耗以外,它还要处理很多其它的东西,比如说要处理一个地方是不是可行走,是不是可以站在前面,是不是有坡这些东西,这一些东西其实是非常消耗CPU时间的。除了主角以外,我们其他的角色尽量使用从服务器发来的位置进行插值计算模拟一下玩家的位置和他的状态,这样也能减少一部分消耗。

5.UI Slate Prepass。在我们的UI比较复杂的情况下会发现UI Slate Prepass这个消耗比较大,UI Slate Prepass主要是用来处理,各个UI的位置以及这些位置刷新的这些东西,当UI的这些组件比较多的时候它是非常消耗的,比较有效的方式其实是可以通过Invalidation Box来缓存Prepass的数据,这样尽量减少位置更新的计算,这个实际测试了一下,还是能带来比较好的效果。还可以尝试用控制台的这个指令Slate.Enable Layout Caching或者Slate.Enable Global Invalidation,这两个也都是比较有效的方式,也能够大量地减少我们的Prepass的开销。

6.音效并发。我们的MMO游戏遇到过大量的音效并发的情况。我们同时和几十只怪进行战斗可能放了一个群攻特效,可能每一个音效可能会被播放了几十次甚至上百次几百次的量级,我们可以通过控制声音的并发来减少它的同时并发的数量,这样也能减少一些爆破音的出现。

7.我们也遇到过一些Skeleton Mesh更新的问题。在一些低端机上,本来它的CPU其实就不是很强,UE4里面如果我们在把骨骼控制在75根之内,并且每一个顶点受到了骨骼这个影响的数量少于4根情况下,引擎默认会使用GPU的蒙皮,它不会使用CPU的 效率会比较高,如果超过这个限制这个时候,它就会使用CPU的蒙皮。对于CPU的蒙皮来讲,第一本身 它对CPU的消耗比较大,第二对于低端机来讲,它肯定会加加重CPU的负担,在我们之前的一个项目里面也遇到过情况,到最后我们是把资源进行了修改,保证骨骼的顶点都在这个限制之内,这样的话GPU的蒙皮比CPU的这个蒙皮的话本身要快非常多。

除了刚才提到的CPU的这些消耗以外其实还有很多GPU的消耗,GPU这个瓶颈主要来源于渲染压力太大,我们也进行了一些分类,比如说Draw Call、材质的复杂度,Shadow,屏幕的分辨率,还有一些后处理效果以及大面积的Alpha叠加。我们针对这些情况会进行一些LOD的处理。

1.Draw Call。对于Static Mesh场景中大量的这些Mesh都是Static Mesh,对于这些Static Mesh我们可以通过Merge Actors 引擎提供的这个功能来合并,尽量把同样的这个Material的Actor合并成同一个Actor。这样的话它在绘制的时候一次就绘制出来了,这个能有效地减少咱们的Draw Call。在移动设备上它每一个这个合并完的Mesh的顶点数应该要控制在65535之内,否则可能会出现问题,可能它会画不出来,还有一个就是UI的的消耗 其实对Draw Call也是消耗的比较大的,我们也进行了一些Sprite的进行合,也进行了一个用的UI Sprite进行,同样的这个Sprite的UI会进行合批,也能减少很多Draw Call。需要注意的就是通过最新的机型发现,随着机型机器的效果,机器的性能越来越好,UI的简单的Draw Call性能的影响其实是越来越小的,但是相反,机器越差的话可能这个影响会越大。

2.材质复杂度。它分为很多的方面,一个就是材质本身它的运算节点,它的采样其他的一些东西,还有就是它本身的加减乘除运算,这些都会影响材质的复杂度,材质节点提供了一个比较好的优化的方法叫Quality Switch,我们就可以使用这个Quality Switch进行高端机和低端机的区分。通过在控制台里面我们就去设置这个Quality Label,它就会让整个这个材质走不同的Quality Switch的分支。我们可以在低端机的分支上处理这些运算的时候,例如我们在处理一些纹理混合的时候是不是可以少混合几张,是不是我们在进行其他的法线运算的时候也可以进行一些其他的优化,通过这个Quality Switch很方便地实现。

材质属性里面的这个Fully Rough其实也会起到很好的效果,同样的渲染起来它其实会 减少很多的Shader的运算,实际效果非常好。不过它可能会对效果有一定的影响,所以使用这个的时候我们可能要一边测试一边使用。另外一个可以通过设置纹理的MipBias,通过这个方式对打开了MipMap的纹理进行纹理的偏移,偏移之后因为本身它是采样的不同层,会对我们的采样的消耗有一定的影响,会有好的效果,本身它也会对填充率有一定的影响。

3.Shadow。移动设备上其实是推荐使用Custom UV的,对低端机上就是影子的开销,尤其是实时阴影它的开销其实是特别大的,我们可以通过这个Shadow Quality控制实时阴影的质量,低端机甚至可以直接把它关掉,只给主角开一个实时阴影,其他的玩家为了我们表现效果可以加一些假的那种面片的阴影上去。

4.Mobile Content Scale Factor和Screen Percentage。这两个对GPU的性能影响非常显著,可以根据不同的档次进行调节,也可以通过这个控制台实时地去切换,需要注意的是因为它本身设置完了以后是会改变渲染的这个Back Buffer的大小从而提高性能的,当我们把它拉伸到这个手机屏幕上去就会发现,这个可能会使画面有一些模糊,它是影响画质的一个选项。使用的时候我们需要注意,当模糊比较厉害的时候可能还需要开AA去解决问题。

5.后处理效果。对于低端机消耗极大的是后处理效果,尤其是大量的后处理,好几个后处理叠加在一起的时候,那么GPU弱的机器,我们其实也可以根据项目的实际需求确定了是不是要把这个后处理关掉,因为这个东西对这个低端机的GPU压力太大。

6.面积的Alpha的叠加。在我们比如说像 多人战斗的时候,会出现多个粒子系统的叠加,这个时候就会导致大面积的Alpha的叠加,这个大面积的Alpha面片的叠加本身导致的结果就是性能会急剧下降,这个肯定是卡在GPU上的,同时它也会导致发热相当严重。这个时候我们可以考虑设置Particle的LOD来减缓一部分问题,那么我们可以在做粒子系统的时候把粒子系统的 LOD 也做了,这个在多人战斗的时候是比较有效的。

7.HLOD、Proxy Level、LOD、Detail Mode、Max DrallDistance。还有刚才提到的Draw Call和Mesh的合并,除了这个Mesh的合并,其实我们还有很多LOD可以结合着使用,引擎提供了这么一种LOD ,一个本身就是单个Actor的LOD这个单个Actor的LOD 它不会减少Draw Call,但是它可以为Actor设置不同的贴图,你给它设置不同的纹理也可以达到效果,达到减少渲染的压力的作用,我们离得比较远的Actor是不是可以给它考虑设一个不受光照影响的贴图,不受光照影响的Material它其实比默认的Default lit 性能要高很多,离远了我们是不是也发现不了区别,那就可以尝试。

HLOD和距离远的Actor它可以有效地减少Draw Call,因为我们可以把多个小的Actor合并成一个HLOD的Actor,那么这个HLOD的Actor它用一张贴图,用一个材质就可以解决问题,这个时候我们会发现Draw Call少了,渲染效率会有大幅的提升。Proxy Level这个代理场景它适合用于特别远的场景,比如说离我们非常远 有这么一个地块上面都有很多的东西,它可以极大地减少Draw Call,只是它的显示效果比较粗糙,同样也意味着它的效率会比较高。

有些时候可能我们觉得有了HLOD了是不是还有必要开这个Proxy Level,它的主要的用途就是说当我们的HLOD 其实它是随着我们要加载的场景再加载出来的,但是如果本身这个场景没有加载出来,其实这个HLOD它也是加载不出来的。举个例子,我们的节能表默认的可能是加载两三百米的场景,有些时候因为我们有飞行坐骑的系统,可能飞行的比较高,一下子可能看出几千米去,你会发现这个时候远处的东西其实并没有用这个 Level Streaming 加载出来,因为太远了。远处就出现了很多比较空旷的情况,这个时候Proxy Level就能比较好地解决问题,它会在在没有加载出真实场景的时候,它先把代理场景加载出来,因为比较远的所以效果我们也是可以接受的,如果觉得不行,这个Proxy Level其实也可以调节多级,根据不同的远近调整效果以达到目的。

在低端机上我们可以设置一些Detail Mode,比如说有一些装饰的一些草和地表的一些细节什么的,没有什么逻辑影响的我们就可以把它减掉缓解一部分GPU的压力,尤其是针对那种带半透明的物体,其实也是比较有效的一种方法。Max DrallDistance它可以控制最远可见的这个Actor的距离,当设置完了这个以后一些比较远的这个Actor它就直接被剔除掉了,它不会显示在我们屏幕上,也是一种比较有效的优化效果。在我们做这个两个项目的优化的时候这几个这些方案其实都是掺杂在一起用的,单独用某一个方案可能都没有办法解决所有的问题,而这个其实也需要一个非常仔细的调节才能做到效率和性能的平衡。

说完了这个效率,再提到一个卡顿,有些时候对于做优化不是非常熟悉的这些,可能会觉得这个卡顿和那个帧率低这两个是一个事情,但这个就是卡顿和那个帧率低它不是一个事情,帧率低可能是因为我们的运算量太大,它的运算量一直大,它可能持续的地比如说十帧、二十帧这样的,那它是帧率低。但是对于卡顿,它是说在突然之间帧率有20帧,或者说突然之间由30帧掉到10帧马上它可能又恢复了二三十帧的这个样子,那么感受是非常差的。

通常有些时候比较有意思,我们在面对外部玩家的时候,可能外部玩家其实很难分得清楚到底哪一个是卡顿,到底哪一个是帧率低,这个时候我们去测试了,可能玩家就反馈 卡的厉害他说的这个卡到底是因为帧率低还是因为出现忽然掉帧的情况,我们其实是要详细地进行去分析的,这个时候我们就用到了Unreal里面的前端工具,这个工具其实它对分析卡顿其实是非常有用的,因为我们可以直接看到每一帧 它的Game线程的波峰波谷,当有出现一个特别特别高的这个卡顿的时候,它这一帧的时间就会花的特别长,我们可以很方便地就把这一帧就是抓下来从这个数据里面去分析,并且定位到到底是因为是什么,这个工具它可以很方便提供咱们的堆栈信息。

在我们优化的过程中其实也对这些优化的点进行了一些分析,大概卡顿主要出现在这么如下的几个方面,一个是GC,一个是资源加载,一个是Actor的这个动态创建。

1.GC。本身GC其实它是一个非常耗时的工作,因为它要遍历这些Object并且发现这些Object 哪一个是有引用的哪一个是没有引用的,哪一个它可以释放的,当我们的Object相当多的时候,本身它这个GC的时间也会跟着增长,在引擎里面提供了一个Max Object Not Considered By GC这么一个选项,默认设置成1的话可以有效地提高整个GC的效率,因为设置成1以后引擎默认创建的一些对象将不再会GC的时候进行计算,从而可以少遍历一些有这个的,这样的话会有一定的效率提升。

当我们的本身的内存不是那么紧张的时候,也是可以考虑GC,稍微时间长一些的话,这样的话它卡顿的现象也能减少,它每一次的GC的时间不会像之前的那么长,那么的这些就会导致频繁的卡顿。GC的卡顿有可能是在我们的那个性能分析工具里面去看到的最高的那么一个。

2.资源加载。尤其需要注意的是我们加载资源的时候,有时候会调用那个Static Load Object,当我们调用Static Load Object的时候,由于引擎没有办法判断这些资源的相关的依赖性,它首先会做的一件事情,它会Flush 异步加载的资源,这就有一个问题,如果我们正在手动或者自动地或者引擎 调用了一些异步加载的函数比如说我们发现现在加载了一个人,那么我远处看见一个人,我需要把它资源加载上来,如果用同步就很慢,同步这个东西它加载的时候就会很卡,我们就会把它设置成异步,异步加载它的这些衣服上的贴图,异步地去加载一些它其它的一些资源之类的。这个时候如果刚好我们有一个其他的功能打开了,比如说一个UI这个Static Load Object,这个时候它就会先把异步加载的资源要求它先加载完成,所有的异步加载就都必须在这一帧马上完成,这个时候就会带来一个极大的卡顿,所以有些时候受这个影响比较大的。

在Level Streaming我们的场景地图在进行切换加载的时候,其实它有一些东西也是异步的,这个时候有些时候我们可能会非常奇怪,我加载了就这么一个Static Load Object的,加载了这么一个小小的这么一个UI为什么会造成这么大的一个卡顿,其实这个时候可能它会有一些其他的这些Level Streaming加载,或者说咱们一个异步加载的一些东西在里面,那么这些东西它才是真正造成卡顿的原因,所以对于Static Load Object的方式,我们在使用中一定要非常非常地小心。

另外打开Texture Streaming可以提高一些纹理的加载速度,一旦打开了Texture Streaming它加载的时候其实会先显示一些比较粗糙的精度的模型,它先从小的加载再把大的加载完了以后才会显示出一些比较精细的就是上面的贴图效果。资源加载这一块,异步加载其实相对来讲较慢,但它能缓解卡顿。所以Static Load Object它同步加载的这个它会加载的比较快,但是卡顿也是比较明显,所以我们其实会倾向于比如说一些需要Static Load Object,尽量我们在加载场景读条的时候把这些东西都加载完,场景里面如果有一些比较大的问题尽量使用异步的加载,就是少使用Static Load Object方式,因为它会有太多的不可控的东西。

3.Actor的动态创建。我们的这个场景里面杀怪可能有一波怪我们放了几个AOE 技能,AOE的技能直接就把他们都杀死了,这个时候它又会重新创建,有些时候因为我们的Actor带着很多的Component,怪身上带了很多的那个组件有不同的功能,还有不同的蓝图组件,这个时候我们去动态创建一个Actor的时候就会造成一定的卡顿,当你的这个Component越多,你卡顿就会越明显,你Component少的时候还好,但是我们会面临着因为刚才提到我们因为涉及到游戏类型,我们可能会进行一些AOE的操作,比如说十几个怪 一下子都杀死了,同时一下子又都加载出来了,那这个时候比较有效的方法第一就是要减少它Component的数量,同时它本身自己的贴图精度也好,它的动作也好,要尽量简单,我们加载它的这个时候相对来讲就会好很多,但是这个不能解决根本问题。

我们也可以减少这个Actor的创建次数,不要频繁地生成和销毁这些Actor,刚才提到的也可以进行分帧加载,我每一帧只加载一个,但是这样又造成一个问题,比如说我十几只怪都死了,那可能我加载出来它这个速度会非常的慢,分成了好几帧可能有一些怪 它就跑了或者其他的感受又不好,所以我们后来把所有的就是重复的比较多的,比如说怪 同样的怪特别多,那么我们就会为它创建一些Actor池,这样的话我们当一个怪死了以后我们不会直接把它Destroy,我们减少动态创建,每次从池里面把它拿出来,当它Destroy的时候我们再放回这个池里,这是可以有效的减少方式,创建这个的时间几乎就是没有消耗的。

要解决卡顿,我认为其实就是创建Actor池可能是一个终结的解决办法,尤其对Actor的比较复杂的一种情况,当然大家如果有一些其他比较好的方法也可以一起来讨论。

刚才提到的就是整个的那个我们的造成帧率低和卡顿的原因,后续还有一个就是对于我们比较重要的其实是一个内存,一个好的消息是我们现在机器 随着机器越来越好,我们现在已经能见到有配置12g的内存的机器了,有没有16g的我还没见着,但这个基本上已经快赶上我们的PC机了,但是同时我们又不得不面对很多低端机,比如说像我们的刚才提到的那个比较老的机器,它可能只有两g或者三g的内存,这些的时候如果我们的内存占用比较大,那么它崩溃的几率就会比较大,很可能我们的东西还没有加载完它崩溃了。

这里面又有几个比较值得注意的东西,就是关于安卓机Armv7和Arm64的区别。ARM64它可用的内存会多一些,如果我们只有一个ARM V7的包,那ARM V7它能够利用的内存可能只到了个2g以内它肯定会崩溃,可能在一个1.45g的时候1.6g的时候可能它就已经非常接近崩溃的边缘了。我们其实就是应该尽量的去使用ARM64的包,ARM64的包其实它的可用内存会多一些,在我们使用内存不太过分的情况下它崩的几率就比ARM V7的要小要小很多了,例如我们使用到的1.7、1.8g的内存可能它也不会崩溃。

我们去查这个内存的工具除了用一些ADB的指令以外,如果我们想去分析到底它占了多少内存,我们用的Memory Report这个log比较多,以4.25这个引擎的log为例,有这么几个东西是我们着重可以去观察的,一个是Process,就是整个进程占用的实际内存是多少,另外的一个是通过引擎这分配的系统内存是多少,还有一个是那个显示所用的内存是多少。当然显示所用的这个内存其实由于它是驱动层面去分配的,有可能也是分配在显存里面或者其他方面的,RHI这部分内存引擎log进行的是一些估算,会有一定的偏差,但是可以给我们一定的作为我们去优化了这么一个参考的依据。

通过除了这几个内存以外,我们可能还会发现它和我们统计出来的系统内存和显示内存加起来,比这个进程实际占用的内存要低一些,那是因为第一有一些第三方库其实也会分配一些内存,这些东西可能不在我们的这个系统内存统计范围内,还有就是我们本身自己运行安卓的SO以及一些其他的这些东西现在也会占用一部分内存,这些都是没有接入引擎统计范畴内的,但是实际又确确实实地占用了它的进程实际的内存之中。

我们常见的一些内存的优化的方式,刚才也提到了一些像我们的这个Level Streaming,用Level Streaming 本身就是可以让我们的地块进行这个分块加载,还有一个就是刚才也提到了就是Texture Streaming。另外的一个就是说通过对于RHI内存,我们可以多去考虑一下纹理的精度到底需要到一个什么样的层面。

1.Level Streaming 。它需要注意的是本身它是根据我们的这个包围盒来进行加载的,那么合理设置包围盒和加载距离是非常重要的,它可以防止在某一个交界处人物来回移动频繁造成场景加载卸载的情况,你会觉得我就在这里走来走去的,会发现它的卡顿就非常的明显,往往都是因为我们的这个Level Streaming。

Actor的序列化以及这些东西造成的一些问题。我们在子关卡的大小和加载之间需要做好权衡,太大了内存太高,太小了这个加载又会频繁那种卡顿,感受也都也都不是非常好。包括在做地形的时候,其实我们也有一些方式可以去 考虑,比如说我们做这个每一个这个子关卡的时候,它如果是一个比较接近于方形的,那在我们使用的时候会比较好用,如果非常不巧你把这个东西做成了一个梯形的,或者做成了一个L型的,这样的话那你会发现,它其实只有窄窄的一溜,但是在整个的这么一个大的Bounding Box里面的话都需要去加载,这一块就是也是需要我们去制作中需要去注意的一个东西。

2.Texture Streaming。由于这个UI它本身Texture一般不设置Mipmap,它是无法从这个 Texture Streaming中获得优化的,除了通过Pool Size去设置这个内存以外,它还可以加快这个Texture的纹理的加载速度,这个刚才已经提到过了。我们设置完了这个Pool Size以后,它可以比较好地去控制整个这个使用的纹理的总量,当有一些超出池的这个Pool Size的设置之外的,它可能会优先的把一些远处的一些东西把它 替换成那个低的Mipmap。

3.纹理的精度。我们可以很清楚地发现每一个就是说纹理的精度是什么,我们可以通过刚才提到的那个 log可以查到每一张纹理它大概占了多少的内存,以及它是多大的,首先要考虑这些纹理有没有减小,比如说我用了2048的贴图是不是要用这么高的,还有我们可以通过设置这个在低端机上设置一些Mobile Reduce Loaded Mips,这个可以把它最顶层的那一层忽略掉,这样的话在它加载的时候也会减少很多的内存。

因为纹理差一半的话它的大小要差了4倍,所以这块的话其实是可以很好地控制它的大小的。这些东西我们都可以在刚才提到的我们的Profiling里面去设置,根据高端机和低端机不同再切换,低端机既然内存小的话,那我们有一些纹理的精度可能降了一些也是可以理解的。

那么刚才也大致讲了一下就是我之前优化过的一些东西,还有一些我关于这个优化的几点思考。

首先就是说优化,我认为应该是尽量的不损失游戏的效果以及功能,那么在不损失这个游戏效果和功能的情况下进行优化了其实才是这个最优的选择。另外还有就是以产品的一个角度思考问题,它需要这个设计资源和技术的共同努力,有些时候如果只靠技术方向解决可能有一些东西不见得解决的非常完美,如果很多能从设计角度去规避的东西才是最优的选择。从设计完了以后,如果从资源的角度再能控制好,那么我们在技术上就可以少用一些刚才提到的技术手段。

优化其实它是贯穿游戏开发始终的这么一个过程,它其实本身就是说是不断地做,不断地优化,往往有些时候可能大家都想是不是我们可以开始的时候不做,我一开始的就是把所有的这些效果都做出来,等到了差不多快上线的时候我再去优化,其实这个时候往往你会发现来不及,因为各种东西涉及到的问题太多。我认为持续的优化它是对产品品质的这么一个不懈的追求,那么我们 为什么要做优化,其实总结成一句话,就是要将更好的效果带给更多的用户,使更多的用户可以体验到我们真正的高品质的游戏,这就是关于优化的一些思考。

谢谢大家!

 

如若转载,请注明出处:http://www.gamelook.com.cn/2020/12/405959

关注微信