部分内容参考了[大天使H5主程死肥仔在LAYA沙龙的演讲PPT],感谢主程陈策的无私分享!
另外参考极光会客厅:大型H5游戏如何登陆微信小游戏及游戏性能优化分享
一、分析工具
1.laya.utils.Stat性能统计面板介绍
2.使用chrome的性能分析器
二、内存优化
参考内存优化方式
1.对象池Laya.utils.Pool
当对象设置为null,不会立即将其从内存中删除。只有系统认为内存足够低时,垃圾回收器才会运行。内存分配(而不是对象删除)会触发垃圾回收。
垃圾回收期间可能占用大量CPU并影响性能。通过重用对象,尝试限制使用垃圾回收。此外,尽可能将引用设置为null,以便垃圾回收器用较少时间来查找对象。有时(比如两个对象相互引用),无法同时设置两个引用为null,垃圾回收器将扫描无法被访问到的对象,并将其清除,这会比引用计数更消耗性能。
2.Handler.create使用了内置对象池管理,因此在使用Handler对象时可使用Handler.create来创建回调处理器。
3.从内存中清理资源
关于clearRes和clearTextureRes,可以参考Texture资源销毁和Laya Image graphics
比如UI上的资源,我是做成界面关闭一段时间后,该界面没有被再次打开,说明这个界面不是个需求常被打开的界面,就可以删除他的资源了。但资源有可能是多界面共用的,所以我还会判断是否被其他界面使用。没有其他界面使用的,就可以删除了。
UI上的动画,动画是多帧的,占的资源会比较多。而且动画一般是做提示作用,显示一次会再打开该界面,也许就不需要该动画了。所以我是对动画处理成关闭界面后,如果没有其他界面正在使用该资源,就立即删除该资源。
对于怪和其他玩家,原来我是做切地图的时候才清除的,因为我们一张地图里的怪的种类是有限的。但后来内存还是太大了,IOS有点顶不住,所以后来改成每个怪被杀后,判断它的资源有没有其他怪也正在使用,没有的话就删除。基本上就是这一波同种类的怪全被杀掉后,就会清除资源。这里说下,这些资源除了图片外,还包括声音
角色是游戏是主角,他们的动作会比较多,每个动作都会是一份资源。如果按怪和其他玩家的资源删除规则肯定是不科学的。我是给每个角色做了资源的列表,但某个动作的资源一段时间没有使用,说明这个动作不是常播放的动作,就可以删除了。技能动画,技能本质上其实和主角的动作资源是类似的,所以也是记录一段时间内没有再次被使用,就会被删除。
把不用的资源都删除了,内存占用还会高,那最直接的方法就是减小资源的大小。比如我们可以把某个图缩小,再在程序里把他放大,这样内存占用会小,表现上会差不多,就是图有可能会糊一些。
对于主角,他是游戏视觉的中心,玩家会一起盯着他,所以我不对主角的资源进行缩小。缩小会影响到游戏的品质。对于游戏里的怪,他们不是视觉的中心,我们把他缩小到原图的67%,再在游戏里放大,为什么是67%呢,因为这样的话在程序里scale设置为1.5就是原设计的尺寸了。
技能一般会比较大,但每一帧的变化也大,在游戏里一闪而过,玩家还没看清会一闪而过了,而且一般同一个技能的颜色也会比较统一,所以可以缩得更小一些,按技能的资源可以缩到一半甚至四分之一。比如,技能= 25% or 50%,武器= 67%,翅膀= 67%
4.关于滤镜、遮罩
尝试尽量减少使用滤镜效果。将滤镜(BlurFilter和GlowFilter)应用于显示对象时,运行时将在内存中创建两张位图。其中每个位图的大小与显示对象相同。将第一个位图创建为显示对象的栅格化版本,然后用于生成应用滤镜的另一个位图
当修改滤镜的某个属性或者显示对象时,内存中的两个位图都将更新以创建生成的位图,这两个位图可能会占用大量内存。此外,此过程涉及CPU计算,动态更新时将会降低性能。
ColorFiter在Canvas渲染下需要计算每个像素点,而在WebGL下的GPU消耗可以忽略不计。
最佳的做法是,尽可能使用图像创作工具创建的位图来模拟滤镜。避免在运行时中创建动态位图,可以帮助减少CPU或GPU负载。特别是一张应用了滤镜并且不会在修改的图像。
5.图片尺寸最好是2的整数幂
这个按钮所用的资源是这样的,它的大小的是138×305。在手机里,图片上传到显存后,宽高会变成2的整数次幂,看图片上的辅助线,一个格子就是256×256的,所以这张图片就会加大成256×512,如果我把图片缩小成右边这个大小,那在显存里,就是128×256,比原来小了3/4。在这里,目的就是把正好大于2的整数次幂的图,缩小一下变成2的整数次幂以内,可以少很多显存。同样的,一个图集,也有可能会有这样的情况,也可以对里边的一些散图缩小,或者换到别的图集的方式让他保持在2的整数次幂内。
6.不在显示区域内的对象不加载
这是我们游戏的主城地图,中间红色框的部分是游戏显示的区域,黄色的是显示区域,也就是角色,NPC等对象在黄色区域内才会显示,在外边的,就会不加载不显示,这里不单单是要记录Visible为false,还要设置为不加载他当前的动作。为什么黄色区域会大于游戏显示区域呢。因为人物的位置一般是脚下的一个点,这个点出到屏幕外时,他可能还有一半的身体还是需要显示的。所以会留出一部分区域。细心的话你们可以看上边的会留得比较小,因为是人物脚底为中心,所以肢底以下还会显示的部分会比较小。
三、渲染优化
参考图形渲染性能
1.优化Sprite
- 尽量减少不必要的层次嵌套,减少Sprite数量。
- 非可见区域的对象尽量从显示列表移除或者设置visible=false。当我们打开一个界面的时候,玩家的视觉焦点会集中在打开的界面上,对于界面旁边的东西,并不会太去关注了。所以,大家有没有发现场景里的人物已经不见了?在弹出界面的时候,把人物,技能特效等隐藏掉,可以减少drawcall。这里要注意的是,隐藏只需要把角色的visable设置为false就可以达到减少drawcall。当然对于已经隐藏掉的人,不再去加载他切换动作的新动作资源,也是一个优化点
- 对于容器内有大量静态内容或者不经常变化的内容(比如按钮),可以对整个容器设置cacheAs属性,能大量减少Sprite的数量,显著提高性能。如果有动态内容,最好和静态内容分开,以便只缓存静态内容。
- Panel内,会针对panel区域外的直接子对象(子对象的子对象判断不了)进行不渲染处理,超出panel区域的子对象是不产生消耗的。
2.优化DrawCall
- 对复杂静态内容设置cacheAs,能大量减少DrawCall,使用好cacheAs是游戏优化的关键。
- 尽量保证同图集的图片渲染顺序是挨着的,如果不同图集交叉渲染,会增加DrawCall数量。
- 尽量保证同一个面板中的所有资源用一个图集,这样能减少提交批次。
用LayaAir编辑好UI界面,可以在层级窗体里看到界面里的子对象,大家注意看每一个子对象前边,都有一个小圆点,这个圆点的颜色代表了他来自哪个图集,相同颜色的圆点代表是同一个图集。对于渲染来说,连续渲染相同图集里的图,只会调用一次drawcall,所以在制作界面的时候,在不影响界面的情况下,把界面里各元件的层级调整一下,可以达到减少drawcall的目的。
这里要说一下, 程序字在游戏里,是会单个字生成图片绘制在动态图集里,所以程序字是同一个图集的。把所有的文字统一放在界面最上边也就是这个列表的最下边,就可以减少drawcall。当然,这个操作不是可以想移就移的,要看要移动的子元件之前是否有前后关系
角色一般都会有一个影子,在我们游戏里影子不是做在角色的图片上的,而是单独立出来的一张图。为了方便操作,我们一般会把人物的图片和影子的图片放到一个容器里,每一个容器就是一个角色。那场景里有两个角色的时候,那这几个图片的顺序就是:影子、角色A、影子、角色B。那就是四个drawcall,如果有N个角色,就有N*2个Drawcall。两个角色的影子是同一张图,一定是同一个图集的。那我们就可以优化下。如果把所有角色的影子放在一个容器里,这个容器在所有角色的下边。那顺序就是:影子、影子、角色A、角色B,那就是三个drawcall。如果是N个角色,就是N+1个drawcall。是不是就可以减少Drawcall了呢。当然,如果要这么做,逻辑就要改得比较复杂,因为要在移动人物的时候,隐藏人物的时候,人物变成半透明的时候,移除人物的时候,都要同时控制影子。我们可以在角色类里加入这些支持。同理,角色头上的名字、血条都要单独放在一层里。
3.关于CacheAs
参考CacheAs静态缓存优化
cacheAs主要通过两方面提升性能。一是减少节点遍历和顶点计算;二是减少drawCall。善用cacheAs将是引擎优化性能的利器。设置cacheAs后,还可以设置staticCache=true以阻止自动更新缓存,同时可以手动调用reCache方法更新缓存。
在对Canvas优化时,我们需要注意,在以下场合不要使用cacheAs:
- 对象非常简单,比如一个字或者一个图片,设置cacheAs=”bitmap”不但不提高性能,反而会损失性能。
- 容器内有经常变化的内容,比如容器内有一个动画或者倒计时,如果再对这个容器设置cacheAs=”bitmap”,会损失性能。可以通过查看Canvas统计信息的第一个值,判断是否一直在刷新Canvas缓存。
/**
* 指定显示对象是否缓存为静态图像。功能同cacheAs的normal模式。建议优先使用cacheAs代替。
*/
public function get cacheAsBitmap():Boolean {
return cacheAs !== "none";
}
public function set cacheAsBitmap(value:Boolean):void {
//TODO:去掉关联
cacheAs = value ? (_$P["hasFilter"] ? "none" : "normal") : "none";
}
注意设置cacheAsBitmap=true,实质上等同于cacheAs的normal模式,并不是bitmap模式。当值为”normal”时,Canvas下进行画布缓存,webgl模式下进行命令缓存。webGL下命令缓存模式只会减少节点遍历及命令组织,不会减少drawcall,性能中等。
4.文字描边
在运行时,设置了描边的文本比没有描边的文本多调用一次绘图指令。此时,文本对CPU的使用量和文本的数量成正比。因此,尽量使用替代方案来完成同样的需求。
对于几乎不变动的文本内容,可以使用cacheAs降低性能消耗。
对于内容经常变动,但是使用的字符数量较少的文本域,可以选择使用位图字体。
5.Text.changeText会直接修改绘图指令中该文本绘制的最后一条指令,这种前面的绘图指令依旧存在的行为会导致changeText只使用于以下情况:
文本始终只有一行。
文本的样式始终不变(颜色、粗细、斜体、对齐等等)。
四、CPU优化
参考减少CPU使用量
1.减少动态属性查找
JavaScript中任何对象都是动态的,你可以任意地添加属性。然而,在大量的属性里查找某属性可能很耗时。如果需要频繁使用某个属性值,可以使用局部变量来保存它:
private function foo():void
{
var prop = target.prop;
// 使用prop
process1(prop);
process2(prop);
process3(prop);
}
另外,项目中尽量减少try catch的使用,被try catch的函数执行会变得非常慢
2.计时器里尽可能不要在循环里创建对象及复杂计算
LayaAir提供两种计时器循环来执行代码块。
Laya.timer.frameLoop执行频率依赖于帧频率,可通过Stat.FPS查看当前帧频。
Laya.timer.loop执行频率依赖于参数指定时间。
Laya.timer.frameLoop(1, this, animateFrameRateBased);
Laya.stage.on("click", this, dispose);
private function dispose():void
{
Laya.timer.clear(this, animateFrameRateBased);
}
当一个对象的生命周期结束时,记得清除其内部的Timer
3.尽量少用autoSize与getBounds
public function getBounds():Rectangle
获取本对象在父容器坐标系的矩形显示区域。计算量较大,尽量少用,如果需要频繁使用,可以通过手动设置setBounds来缓存自身边界信息,从而避免比较消耗性能的计算
var sp:Sprite = new Sprite();
sp.autoSize = true;
sp.graphics.drawRect(0, 0, 100, 100, "#FF0000");
Laya.stage.addChild(sp);
autoSize在获取宽高并且显示列表的状态发生改变时会重新计算(autoSize通过getBoudns计算宽高)。所以对拥有大量子对象的容器应用autoSize是不可取的。如果设置了size,autoSize将不起效。
Laya.loader.load("res/apes/monkey2.png", Handler.create(this, function()
{
var texture:Texture = Laya.loader.getRes("res/apes/monkey2.png");
var sp:Spirte = new Sprite();
sp.graphics.drawTexture(texture, 0, 0);
sp.size(texture.width, texture.height);
Laya.stage.addChild(sp);
}));
使用Graphics.drawTexture并不会自动设置容器的宽高,但是可以使用Texture的宽高赋予容器。毋庸置疑,这是最高效的方式。
4.根据活动状态改变帧频
帧频有三种模式,
Stage.FRAME_SLOW维持FPS在30;
Stage.FRAME_FAST维持FPS在60;
Stage.FRAME_MOUSE则选择性维持FPS在30或60帧。
有时并不需要让游戏以60FPS的速率执行,因为30FPS已经能够满足多数情况下人类视觉的响应,但是鼠标交互时,30FPS可能会造成画面的不连贯,于是Stage.FRAME_MOUSE应运而生。
5.使用callLater
callLater使代码块延迟至本帧渲染前执行。如果当前的操作频繁改变某对象的状态,此时可以考虑使用callLater,以减少重复计算。
考虑一个图形,对它设置任何改变外观的属性都将导致图形重绘:
var rotation:int = 0,
scale:int = 1,
position:int = 0;
private function setRotation(value):void
{
this.rotation = value;
update();
}
private function setScale(value):void
{
this.scale = value;
update();
}
private function setPosition(value):void
{
this.position = value;
update();
}
public function update()
{
console.log('rotation: ' + this.rotation +
'\tscale: ' + this.scale + '\tposition: ' + position);
}
setRotation(90);
setScale(2);
setPosition(30);
控制台的打印结果是:
rotation: 90scale: 1position: 0
rotation: 90scale: 2position: 0
rotation: 90scale: 2position: 30
update被调用了三次,并且最后的结果是正确的,但是前面两次调用都是不需要的。
尝试将三处update改为:
Laya.timer.callLater(this, update);
此时,update只会调用一次,并且是我们想要的结果。
6.图片/图集加载
在完成图片/图集的加载之后,引擎就会开始处理图片资源。如果加载的是一张图集,会处理每张子图片。如果一次性处理大量的图片,这个过程可能会造成长时间的卡顿。
在游戏的资源加载中,可以将资源按照关卡、场景等分类加载。在同一时间处理的图片越好,当时的游戏响应速度也会更快。在资源使用完成后,也可以予以卸载,释放内存。
7.每帧只计算一部分
我先介绍一下我们的战斗,在野外的战斗,是由客户端计算杀死这波怪的过程,再由服务端验证这个杀怪过程的合法性,再给出杀怪的收益。计算的过程是这样的,主角遇到一个怪,向他走过去,走到可距离怪小于释放技能的攻击范围时,释放技能,然后要用敌我双方所有与战斗相关的属性,计算出是否命中,是否爆击,是否触发各种效果,每个效果会不会加某些属性之类。最后得到怪物最终伤害,最终双方属性有什么变化等。这一系列的计算,虽然是纯数学的计算,但因为数量的内容很多,所以也是一个比较耗性能的操作。这只是有一只怪的情况。一般我们的怪是都不是一只一只出来的,而我们的主角的技能也大部分不是单体攻击技能。所以技能会同时攻击两个目标,这时,这两个目标就会分别计算伤害。这还只是两只怪,我们游戏里一般一出来就是很多只怪,在某些玩法里出现几十甚至近百只怪。真的,免羊大战这个玩法就是看谁的场景里先有100只怪谁就输。那计算量就很大了。那我们是否可以只计算一次伤害,另一只怪就用前一只怪计算的伤害来减少CPU的运算量呢?不行,第一,是否命中,是独立运行的,我们不能做成命中就所有的怪都命中中,不命中就所有的怪都不命中,这样表现会很糟糕。第二,怪和怪不一定是相同的怪。第三,攻击的结果不只是造成伤害,还有加BUFF,会给敌我双方属性做出变化。那我的属性变化了,对第二只怪有可能计算出来的值就不一样。
那我怎么做呢。我是把每只要被攻击的目标,放到一个待计算列表里,每一帧,只拿出若干个进行计算,这样有的怪会晚几帧才会飘出伤害,在视觉上是没有区别的。但就不会在释放技能的时候卡一下。
8.Laya.Stage.getTimeFromFrameStart
/**
* 获得距当前帧开始后,过了多少时间,单位为毫秒。
* 可以用来判断函数内时间消耗,通过合理控制每帧函数处理消耗时长
,避免一帧做事情太多,对复杂计算分帧处理,能有效降低帧率波动。
*/
public function getTimeFromFrameStart():Number {
return Browser.now() - _frameStartTime;
}
数据层的数据被修改后,会抛出通知。关注该数据的正在显示的界面就会收到该通知,然后去取到数据刷新界面的显示。这个刷新界面的显示内容就不可预估了,有可能是一个复杂界面,要刷新也许还会删除一些显示对象,添加一些显示对象。性能消耗就可大可小。
如果这时服务服同时推过来若干条数据要处理,那性能消耗就完全不能预计了。在这里要做的优化是,什么情况下服务端会在短时间内推大量的数据过来,如果有可能,就要对协议的方式做修改,以减少推送的次数的大小。如果不能做修改,那就要把服务器推过来的数据放到一个队列里,每帧只处理一部分。Laya里是有一个方法,在Stage下的,可以取得当时时间距离当前帧开始的时间已经经过了多少毫秒,我们可以利用这个方法判断处理协议,直到这帧快要达到这帧预期的时间,就余下的协议放到下一帧去处理。
这样会不会让协议处理被慢呢?不会,因为原来在同一帧里去处理的话,需要的时间也是大于一帧,也是要卡在那里等协议处理完,也是等了这么久。放到下一帧,也是过了这么久才处理完。不同的是,原来是卡着游戏,其他一切都动不了拉在等处理完,现在是不卡着让用户感觉不到在等的处理完。所以效果会更好些。
但这里要注意,就算其他逻辑太多引起本帧没有剩余时间去处理一条协议了,也至少要处理一到两条协议,不然在比较差的设备上,也许会因为机器卡,就永远都没法处理协议,表现上就会和没收到服务端数据一样了。
9.View.createView
比如界面里有个水平滚动的功能,里边每一页都是道具的列表,每个道具列表里又有若干层。在第一次打开这个界面的时候,我们的程序就会创建这个界面的对象,就需要创建很多显示对象,这时会卡住很久。玩过我们游戏的人就会发现,这个界面打的时候,会先显示底上边的按钮,然后显示第一页的部分道具格子的底,然后再是另一部分道具格子的底,然后是显示一部分道具图标,再是另一部分道具图标。我说这么长,然后也就是在不到一秒的时间内,只是大家可以感觉得到一部分一部分的显示。就不显得卡。我是改写了View里的createView方法,会每帧只初始化若干个显示对象。
五、加载优化
分包