游戏性能优化
一、游戏性能——白鹭:
1、性能糟糕原因:
1)帧频很低(即FPS低,正常60),量化为:在特定设备上的帧频是XX帧,其中JavaScript逻辑开销XX毫秒,渲染开销YY毫秒。
2)设备发热,量化方法:首先将设备充满电,然后统计游戏在XX分钟后的剩余电量。由于耗电量和发热基本成正比,所以解决耗电问题,发热问题也同步得到解决。
3)不定期卡顿:一定要记录卡顿是否存在规律,比如①是播放动画的瞬间,②打开UI面板的瞬间,③毫无规律
2、帧频低和发热主要有如下原因:
1)渲染内容过多
2)渲染方式不当
3)计算开销过大
4)大量创建对象
渲染内容和渲染方式不当最终影响是可以在引擎渲染层解决——理解WebGL底层的渲染原理
计算开销过大和大量创建对象都是在用户逻辑的JavaScript层去解决——了解JavaScript底层的一些原理
对应的优化:
1、渲染内容过多:
在屏幕之外的内容,可以设置为隐藏,不要渲染。如:①打开UI弹窗时,可以吧游戏背景隐藏,同样可以节省大量的渲染开销。
2、渲染方式不当:
底层原理:
统计游戏所有显示对———一次性提交所有的“形状数据”———设置渲染模式———执行渲染批次———设置渲染模式———执行渲染批次———设置渲染模式———执行渲染批次
2D是一次性提交所有的数据,然后设置渲染模式,执行渲染批次,在设计渲染模式,在执行渲染批次。如果你能保证渲染模式这东西是没有发生变化的,就可以一次尽可能多地渲染。
如:
重度游戏的典型UI,DrawCall是30,这种游戏可以做很多优化,就是把所有的图片、文字合成一张纹理集。不要将动态变化的内容合拼。
做UI时尽可能把所有的动态内容放在最上层,把图片放在下层,并将这些图片合成纹理集。
3、计算开销过大优化:
1)对骨骼动画使用缓存,优化骨骼开销
2)避免大量的数学计算与浮点数计算
3)逻辑帧与渲染帧分离;这个提升是比较明显的,因为很多游戏都是做30帧的,但是现在有些是60帧,所以要做一些逻辑帧与渲染帧分离,逻辑上可以是15帧,然后渲染上做60帧,那么逻辑的开销就可以少很多。
逻辑帧与渲染帧分离:
4、大量创建对象:
1)JavaScript虚拟机有一个特,就是对象创建的开销远远大于对象计算的开销
2)大量的创建对象会导致更频繁的GC(GC:垃圾回收),而GC会导致游戏不定期卡顿
3)不要在主循环中创建任何对象,强烈建议游戏中的人物、怪物、节能特效统统做成对象池,这样可以大幅降低游戏的不定期卡顿现象的出现。
主循环指的是:游戏都是由更新状态、处理数据、播放音乐、更换地图和处理动画来构成。而能引起这种的更新一般是由两种行为引起:
1)时间驱动(用户输入)
2)固定时间的FPS(每秒帧数)
而最能解析游戏主循环的就是固定的FPS(每秒帧数),例如:每次刷帧,地图向下移动0.1个单元格,人物向上移动0.2个单元格
4、一个常用于测试每秒钟创建了多少个对象的函数,数量:120以下最好
(function() {
var count = egret.$hashCount;
setInterval(() => {
var newCount = egret.$hashCount;
var diff = newCount - count;
count = newCount;
console.log(diff);
}, 1000)
})();
函数原理:
它就是实现了每秒去拿一个hasCount跟上一个它就是实现了每秒去拿一个hashCount跟上一个hashCount作对比,这个hashCount是由白鹭引擎呢内部API,用于统计引擎对象的创建数量。
如果hashCount>120,如何解决:
只需要在引擎的HasObject的构造函数这里添加一个断点,在运行时去检查调用堆栈就可以了。
5、白鹭的3D引擎的核心功能及内部优化技巧
1)egret3D内部的所有资源都采用GLTF文件格式。这是一种对OpenGL ES、WebGL非常友好的3D内容格式标准。
2)面向实时渲染,尽量提供可直接传输给图形API的数据格式,而不再需要反序列化。
3)3D领域的JPEG。
在egert3D内测版本中,在3D引擎加载一个模型文件,过程:
首先加载模型文件———解析模型文件———生成WebGL所需要的数据格式———提交到GPU
缺点:
1、模型解析速度,加载速度都比直接是WebGL所需要的数据格式时的速度慢。
2、内存也比直接WebGL需要的数据格式时的占用多。
在Egret3D正式版中,加载一个模型文件:
加载模型文件(与GPU想要的文件格式几乎一样———GLTF)——————提交到GPU
优化了:
1、减少了文件的解析与重新生成新的数据格式。
优点:
1、比内测的模型解析速度快。
2、比内测的加载速度快。
3、比内测的内存占用低。
二、游戏性能优化——egret官网优化
一、简介:
1、通过几个方面优化移动端游戏:
1)少使用Alpha混合(即透明度)。
2)显式停止计时器,让它们准备好进行垃圾回收。
3)在不需要触摸交互性时显式禁用触摸交互性。
4)使用事件侦听器并在不需要时删除这些侦听器。
5)合理使用dispatchEvents函数(自定义事件)。
6)尽可能重用对象,建立对象池,而不再创建的对象对其执行垃圾回收。
7)多次调用类属性时,避免直接使用this.att,要建立局部变量赋值。
8)Event.ENTER_FRAME(帧事件)数量控制
9)减少不必要的引用。
10)减少显示对象的旋转、缩放。
11)使用SpriteSheet合并的图片尺寸要优于单张图片的总尺寸,尤其是带透明通道的。
12)在Http请求中,加载单个文件速度要优于加载多个文件。
改变性能的常规方法:
1)降低内存的使用量。
2)降低CPU的使用量。
3)是否可以调用GPU。
2、运行时代码执行基本原理:
在引擎的设计中,Egret底层使用了"弹性跑道模型",即各种操作都是针对每"帧"发生的。
例如:我们指定帧速度30帧/秒,则运行时会尝试使每个帧的执行时间隔1/30秒。
每个帧循环包括两个阶段,三部分:enterFrame事件
、事件
和呈现
。
第一阶段:包括两部分centerFrame事件
和事件
,第一部分是派发一次enterFrame事件
,第二部分是调度运行时的事件
。
第二阶段:是第一阶段所有事件调度完后会执行clear后,帧循环的呈现
阶段开始。此时,运行时将计算屏幕上所有可见元素的状态并将其绘制到屏幕上。
然后,此进程重复,就像赛跑者围绕跑道奔跑。
①有些情况,一半时间运行帧循环事件处理函数和应用程序代码,另一半事件发生呈现。
②有些情况,应用程序代码会通过增加云运行事件并减少用于呈现的事件来占用帧中多一半的可用时间(即应用程序的时间超过1/2帧)。
③有些情况,特别是对于混合模式等复杂的可见内容,呈现需要的时间会多于1/2帧的时间。
由于各阶段需要的实际时间是灵活的,所以帧循环常称为弹性跑道
。
如果一个帧循环需要的时间超过1/30秒,则运行时不能以每秒30帧的速度更新屏幕,如果帧速率减慢,将影响体验效果,最乐观情况是动画断断续续,如果情况更糟糕,可能画面停止,或程序崩溃。
3、体验反馈与实际性能:
根据操作的运行时与创建对象实例数来度量游戏性能:
1)动画是流畅还是断断续续?
2)音频是连续播放还是暂停再恢复播放?
3)键入时,文本输入保持同步还是有些延迟?
4)如果单击,会立即发生某些情况还是存在延迟?
5)PC应用程序运行时,CPU风扇声音是否会变大?
6)在便携式计算机或移动设备上运行应用程序时,电池是否会很快好尽?
7)开启性能调试面板
二、内存优化
1、显示对象:
Egret包含多种显示对象,要限制内存使用量,需要选择合适的显示对象:
1)对于非交互的简单形状,使用Shape。
2)对于没有重绘需求但有子节点,使用DisplayObjectContainer。
3)对于有重绘需求而且有子节点的,使用Sprite。
2、重用对象:
重用对性能与内存非常重要。
需要密集的创建对象,要引用对象池;例如:做一款打飞机类型游戏,进入战斗前,飞机、怪物、掉血特效等对象提前初始化,在过程中实时提取,而不是实时创建。
尽管使用对象池具有性能优势,但它的主要好处是让内存管理工作更容易。如果内存利用率无限制地增长,对象池可预防该问题。此技术通常可提高性能和减少内存的使用。
3、释放内存:
JavaScript中,GC在回收内存时,首先判断该对象是否被其他对象引用。在确定没有其他引用,GC在特定条件下进行回收。
1)删除对象的所有引用确保被垃圾回收器回收。
2)删除资源RES.destroyRes(“”)。
3)暂停清除计时器clearInterval()
、clearTimeout()
、Timer.stop()
。
4、使用位图(即:Bitmap):
位图的创建过程中,需要考虑时机,创建是一个消耗内存与CPU的过程。大量的位图创建会使你内存占用快速增长,导致性能下降,可以使用对象池优化创建销毁。
针对分辨率制作相关素材要比适配最大分辨率节省内存,并且减少由程序自适应带来性能下降。(是否一张张拼成要比一幅大的性能要优)。
宽高<=1024*1024,这不是针对内存占用,但是它影响兼容性。
5、文本:
TextField减少对于描边的使用(stroke)。
TextField中使用cacheAsBitmap,可以减少重绘次数。
固定文字段落应当使用位图避免文字重绘产生开销(即:BitmapText)。
6、事件模型与回调:
Egret是基于事件模型的。这里主要指出事件模型值得注意的事项。
dispatchEvent()
方法循环访问注册列表,并对各个注册的对象调用事件处理函数方法,以下代码说明过程:
//触发
this.dispatchEvent(new egret.event(egret.event.ADDED_TO_STAGE));
//监听
this.addEventListener(egret.Event.ADDED_TO_STAGE, this.test, this);
在每次触发时,会实例化Event
对象,这样会占用更多内存,当侦听Event.ENTER_FRAME事件时,将在各个帧上为事件处理函数创建一个Event对象。在捕获和冒泡阶段(如果显示列表很复杂,此成本会很高),显示对象的性能可能会特别低。
三、CPU优化:
1、触控交互:
禁用不必要显示对象的触摸交互。
使用交互对象(例如MovieClip或Sprite对象时),尤其是屏幕上显示许多交互对象,当他们重叠时,检测触摸交互可能会占用大量的CPU资源。避免这种情况的一种简便方法是对不需要任何交互的对象禁用交互行为。以下说明禁用触摸交互:
var sp:egret.Sprite = new egret.Sprite();
this.addChild(sp);
sp.touchChildren = false; //确保子孙是否接受触摸事件,默认true
sp.touchEnabled = false; //此对象是否接受触摸事件,默认true
还有一种方式禁用对象的touchEnabled
,父容器创建如:遮罩容器并侦听点击事件,寻找对象坐标标点——要额外写逻辑,就是判断触摸的坐标点是对应哪个对象的坐标点。
2、JavaScript:
javaScript是一种解释型语言,执行速度要比编译型语言慢得多,随着作用域中的作用域数量的增加,访问当前作用域以外的变量的时间也在增加。所以,访问全局变量总是比访问局部变量要慢,因为需要遍历作用域链。只要能减少花费在作用域链上的事件,就能增加脚本的整体性能,对于写法的技巧非常重要,下面是一些简单常用技巧。
对于v8虚拟机的优化方法是:避免动态添加属性与修改变量类型,好处是减少创建新类的开销,v8中当试图修改动态变量或属性时,虚拟机会把function
类型缓存为一个固定的C++类并触发虚拟机的重新编译。当你使用TypeScript
显而易见的好处是类型的声明以及语法规范会使你避免这种情况发生。
1)类方法中,将this赋值给另一个临时变量。
2)在循环中,尝试改进写法,减少读取次数。
var len:int = array.length;
for (var i:int = 0; i < len; i++) {
}
3)避免双重解释,如:eval函数
会使javaScript创建解释器,产生额外的性能消耗。
如:
eval("alert(\'hello worid\')"); //避免
var sayHi= new Function(“alert(\'hello world\')”); //避免
setTimeout(“aler(‘hello world’)”); //避免
4)推荐使用正则表达式处理字符串遍历。
5)避免使用[“property”]访问对象属性,改为Object.property。
6)创建对象var obj:Object = {"key":"value"} > var obj:Object = {} > var obj:Object = new Object()。
7)字符串类型 “” > String() > .toString() > new String()。
8)类声明属性不宜过多(< 64),少继承,多引用。
引用:
var obj:Object = {};
var obj2:Object = obj;
//即把obj复制到obj2,但两个变量是指向同一个地址,所以其中一个变量属性值改变了另一变量也会跟着改变(值是number类型除外)。
继承:
var obj:Object = {};
obj.name = \'hang\';
var obj2:Object = new obj();
console.log(obj2.name); //hang
//即obj2把obj的所有可继承的属性都复制了并且obj2和obj的地址是不一样的,改变其中一个不会影响另一个变量。
9)代码中Getter
、Setter
、Try-catch
会使性能下降。
10)请保持数组中类型的一致。
3、计时器与enterFrame事件(帧事件):
显式停止定时器,移除enterFrame侦听。
将程序中Timer对象与enterFrame注册数量降至最少,事件中尽量减少对显示对象外观的更改。
每帧在运行时将为显示对象在内部注册enterFrame事件,但这样做将使每帧执行更多代码,如果超出处理范围,程序会出现卡顿。可以考虑使用一个集中的enterFrame处理函数,通过集中同类代码,更容易管理所有频繁运行的代码。
如果使用Timer对象,也将产生与多个Timer对象创建与调度事件相关的开销。减少或合理设置触发事件,对于性能提升很有帮助。停止未使用的Timer。
4、后台对象:
移除显示列表中的对象,而不是visible = fals, 对象仍在父级显示列表,某些功能依然在遍历这个列表。
避免一些后台对象参与逻辑,例如一些已经移出显示列表的对象,是否需要碰撞检测,距离运算等。
适当延长检测时间,例如:碰撞间隔,非需要特别精确的时候使用简单的矩形碰撞。
5、动画:
对于简单的动画,序列帧的性能更佳。
使用StalingSwf制作动画时,导出文件之前删除合并fla中多余帧,可以减少json体积。
处理动画移动,使用帧时间差计算而不要使用频率,不要相信频率,它在各种环境中时不稳定的。
四、重绘优化:
1、渲染对象:
要改进渲染,务必考虑显示列表的嵌套。每个显示对象都是独立的,当对象放入显示列表后参与整个渲染过程。
绘制过程:由内而外
, spr > 文档类 > 舞台。
Egret版本大于2.5,引擎提供了自动的脏矩形,极大提高渲染能力,无需手动设置。
脏矩形:
设置了脏矩形的区域引擎不会每帧都对显示列表中的显示对象进行重绘,大大的提高了渲染能力。
//关闭自动脏矩形
显示对象.stage.dirtyRegionpolicy = egret.DirtyRegionPolicy.OFF;
//开启自动脏矩形
显示对象.stage.dirtyRegionpolicy = egret.DirtyRegionPolicy.ON;
2、显示优化:
涉及频繁在Stage添加移除对象并且不关心ADDEDTOSTAGE(对象本身被添加到显示列表时触发)与REMOVEfRMOSTAGE(对象本身从显示列表中移除时触发)事件时,可以进行addChild和removeChild优化,减少逻辑判定。
3、Alpha混合:
使用alpha实行时避免使用要求alpha混合的效果,例如:淡化效果。当显示对象使用alpha值混合处理时,运行时必须将每个堆叠显示对象的颜色值与背景色混合起来,以确定最终的颜色。因此,alpha值混合处理可能比绘制不透明颜色占用更多的处理器资源。这种额外的计算可能影响慢速设备上的性能,尽可能避免使用alpha属性。
4、帧频速率:
稳定的帧率是游戏性能最重要的表现,非动作游戏降低游戏帧率可以大幅提升性能。
this.stage.frameRate = 30; //能被60整除的数
使用egret.callLater,egret.serTimeout自定义分帧管理器等来实现功能的分帧延时处理。
例如:
①切换界面时,界面显示与数据填充进行一个分帧或者延时处理来保证UI切换时的流畅性。
②当同一帧创建多个显示对象时,可进行分帧处理,保证帧率稳定。
侦听休眠与激活状态改变帧率与动画:
this.stage.addEventListener(egret.Event.ACTIVATE, this,onActive, this);
this.stage.addEventListener(egret.Event.DEACTIVATE, this.onDeactive, this);
休眠中停止动画与呈现相关内容,在程序被激活时重新启动。
5、位图缓存
在适当的时候对多位图容器使用位图缓存功能。
选择cacheAsBitmap可实现良好的优化。此功能对渐变、多段落文字、多位图、9宫格等有显著提高呈现的性能,但是需要占用大量的内存。
当显示对象内部是实时改变的,启动位图缓存或获得相反的效果。当每帧运行时必须更新缓存位图,然后屏幕重绘该位图,这一过程需要消耗许多CPU。
仅当缓存的位图可以一次生成,且随后无需要更新时,适合使用位图缓存功能。
对Sprite显示对象开启位图缓存后,缩放、移动、修改XY属性不会导致重新生成,但是修改Sprite内部子项将会导致重新生成缓存。
合理使用位图缓存,可以极大程度提高渲染性能。
五、网络优化
1、资源划分
减少界面层次,多层组合为独立位图是目前高性能的方法。界面有时会分为单色背景,纹理,边框,子项边框等这种设定对于网络与程序性能没有提高,合并以上图层会对你应用带来质的飞跃。
合理的根据游戏进程去加载所需要资源,常见的方法有:
1)每次场景变换只加载所需资源。
2)每次点击界面入口按钮加载资源并缓存。
3)初始化应用值只加载可能曲剧全局使用与当前可能需要的。
4)分块分组资源加载。
2、加载通信
当每次发起的HTTP请求或有协议头,确认过程,返回数据,优化合并文件减少请求数可以显著提高网络性能。
资源服务器开启GZIP压缩,提高载入速率。
对于一些有透明通道的png图片格式,可以使用压缩软件进行一定比例压缩,会有非常大的提升空间。对于没有透明通道的资源推荐使用jpg已达到提升网络加载。
对于MP3,尽可能的减少其采样率与码率。
使用egret compress_json
命令压缩json文件,使体积减少。
加载中的显示对象,给予显示对象预设位图,这也许并不能带来网络性能提升甚至有所下降,但是这样的修改对于用户体验是极佳的。
如果不能做预加载处理,用户并不了解屏幕中呈现的最终效果,也许认为游戏有问题,给与用户感知思考是非常好的体验。
三、游戏开发性能优化:
一、游戏优化的四个考虑方向:
1、网络和IO:
1)异步加载资源。
2)分帧处理网络包进行处理。
3)限制发包频率。
4)适当拆包或者合包。
2、CPU:
1)缓存数据。
2)异步计算数据(分帧或者多线程)。
3)采取合理算法和数据结构。
3、内存:
1)动态加载和卸载资源。
2)合理规划美术资源。
4、GPU:
1)优化美术资源。
2)优化Shader。
3)对不同平台使用不同的格式或处理方案(如:IOS和安卓)。
1、CPU:
引发的问题:
1)由于短时间内计算量太大,导致画面流畅性降低,俗称跳帧。
2)发热严重,耗电量高。
常见优化手段:
1)将计算分到多个逻辑中进行计算,避免短时间内的性能超过负荷,俗称分帧(time-slice)
。
2)将可以缓存的数据尽可能的缓存起来,避免重复计算和重复分配内存,常见的示例为内存池
。
3)使用合理的算法和数据结构,比如:冒泡排序和直接插入排序在整体数组比较有序的情况下效率大大好于快速排序,把快速排序替换成是优化程序排序效率的一个常见的思路。
2、GPU:
引发的问题:
1)发热严重,耗电量高。
2)FPS降低。
常见的优化手段:
1)优化美术资源,比如合理规划图集,约定好模型的最大三角形面数,制作合理的粒子效果规范。这个可以说是游戏优化中最重要的一个,因此,技术美术在游戏开发中作用巨大。
模型的最大三角形面数:
在3D游戏中才有。
2)简化或者优化着色器(shader),如在游戏开始前就对shader进行编译和加载。
3)使用Batching,尽量减少DrawCall。
4)使用引擎推荐的压缩格式。
3、IO和网络:
引发的问题:
1)网络延迟甚至掉线。
2)加载资源导致的跳帧。
3)加载时间过长。
常见的优化手段:
1)使用独立的线程进行加载,有些引擎还能利用协程(如:Unity)。
2)减少网络包里面的冗余数据。
3)合并小包,减少请求数据的次数。
4)分帧对回包进行处理。
5)限制一定事件内的发包率。
4、内存:
引发的问题:
1)闪退和卡死。
常见的优化手段:
1)动态加载和卸载资源,比如在游戏内的时候,我们可以把游戏外的一些UI图集卸载掉。
2)降低资源质量或屏幕分辨率,这是有损优化,一般作为最后的手段。
四、Egret性能优化之优化渲染
1、Egret在内核中是如何来处理渲染部分的?
MainLoop ——> EnterFrame(帧事件) ——> clear ——> stageUpdateTransForm ——> stageDraw。
Egret每刷新一帧的时候,会执行四步操作。
1)执行一次EnterFrame,此时,引擎会执行游戏中的逻辑。并且抛出EnterFrame事件。如果在这里编写了大量消耗性能的代码,那么游戏的帧频就会开始下降。
2)引擎会执行一个clear,将上一帧的画面全部擦除。
3)Egret内核会遍历游戏中所有的DisplayObject,并重新计算所有显示对象的transform——visible = false的显示对象也会参加计算。
4)将所有的图像全部draw到画布中。
2、了解了egret的渲染机制,优化游戏:
1)不想要的DisplayObject,请removeChild掉,如果是设置了它的visible属性的false,确实这个显示对象不会被渲染出来,但是,它还是会参与到第三步的计算过程。所以也无形中增加了性能的开销。如果某一个图像被其他图像遮蔽,那么你就需要移除被遮盖的对象。
2)太多的显示对象不仅会在第三步会消耗性能,更重要的是,在第四步的时候,会严重影响性能,让帧频下降。
①可以将画面中的元素进行合并,合并不是将两个Bitmap塞到一个Sprite中,这样病不起作用,无论是嵌套好事并列,都会消耗大量性能。如果可以,最好调整游戏元素图片的拆分方式,尽量减少DisplayObject数量。
②使用cacheAsBitmap,让你的矢量图在运行时以位图形式进行计算。这回大大减少你的矢量图运算。
3)尽量不要在EnterFrame事件中做过多的操作,EnterFrame事件派发太频繁了。
4)善用脏矩形是一种非常高效的优化手段,但它是把双刃剑。用的好,性能飙升,用不好,自取灭亡。