最近对 html5
小游戏有点兴趣,由于我感受未来这个东西或许是前端一个重要的应用场景,例如如今每到某些节假日,像支付宝、淘宝或者其余的一些 APP
可能会给你推送通知,而后点进去就是一个小游戏,基本上点进去的人,只要不是太抵触,都会玩上一玩的,若是要是刚好 get
到用户的 G
点,还能进一步加强业务,不管是用户体验,仍是对业务的发展,都是一种很不错的提高方式。html
另外,我说的这个 html5
小游戏是包括 WebGL
、WebVR
等在内的东西,不只限于游戏,也能够是其余用到相关技术的场景,例如商品图片 360°
在线查看这种,之因此从小游戏入手,是由于小游戏须要的技术一应俱全,能把游戏作好,再用相同的技术去作其余的事情,就比较信手拈来了前端
查找资料,发现门道仍是蛮多的,看了一圈下来,决定从基础入手,先从较为简单的 canvas
游戏看起,看了一些相关文章和书籍,发现这个东西虽然用起来很简单,可是真想用好,发挥其该有的能力仍是有点难度的,最好从实战入手html5
因而最近准备写个 canvas
小游戏练手,相关 UI
素材已经搜集好了,不过俗话说 工欲善其事必先利其器,因为对这方面没什么经验,因此为了不过程当中出现的各类坑点,特意又看了一些相关的踩坑文章,其中性能我感受是必需要注意的地方,并且门道不少,因此整理了一下web
requestNextAnimationFrame
进行动画循环setTimeout
和 setInterval
并不是是专为连续循环产生的 API
,因此可能没法达到流畅的动画表现,故用 requestNextAnimationFrame
,可能须要 polyfill
:canvas
const raf = window.requestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.mozRequestAnimationFrame
|| window.oRequestAnimationFrame
|| window.msRequestAnimationFrame
|| function(callback) {
window.setTimeout(callback, 1000 / 60)
}
复制代码
若是只是简单动画,那么每一帧动画擦除并重绘画布上全部内容是可取的操做,但若是背景比较复杂,那么可使用 剪辑区域技术,经过每帧较少的绘制来得到更好的性能api
利用剪辑区域技术来恢复上一帧动画所占背景图的执行步骤:数组
context.save()
,保存屏幕 canvas
的状态beginPath
来开始一段新的路径context
对象上调用 arc()
、rect()
等方法来设置路径context.clip()
方法,将当前路径设置为屏幕 canvas
的剪辑区域canvas
中的图像(实际上只会擦除剪辑区域所在的这一块范围)canvas
上(绘制操做实际上只会影响剪辑区域所在的范围,因此每帧绘制图像像素数更少)canvas
的状态参数,重置剪辑区域先绘制到一个离屏 canvas
中,而后再经过 drawImage
把离屏 canvas
画到主 canvas
中,就是把离屏 canvas
当成一个缓存区。把须要重复绘制的画面数据进行缓存起来,减小调用 canvas
的 API
的消耗浏览器
const cacheCanvas = document.createElement('canvas')
const cacheCtx = cacheCanvas.getContext('2d')
cacheCtx.width = 200
cacheCtx.height = 200
// 绘制到主canvas上
ctx.drawImage(0, 0)
复制代码
虽然离屏 canvas
在绘制以前视野内看不到,但其宽高最好设置得跟缓存元素的尺寸同样,避免资源浪费,也避免绘制多余的没必要要图像,同时在 drawImage
时缩放图像也将耗费资源 必要时,可使用多个离屏 canvas
另外,离屏 canvas
再也不使用时,最好把手动将引用重置为 null
,避免由于 js
和 dom
之间存在的关联,致使垃圾回收机制没法正常工做,占用资源缓存
CSS
若是有大的静态背景图,直接绘制到 canvas
可能并非一个很好的作法,若是能够,将这个大背景图做为 background-image
放在一个 DOM
元素上(例如,一个 div
),而后将这个元素放到 canvas
后面,这样就少了一个 canvas
的绘制渲染dom
CSS
的 transform
性能优于 canvas
的 transfomr API
,由于前者基于能够很好地利用 GPU
,因此若是能够,transform
变幻请使用 CSS
来控制
建立 canvas
上下文的 API
存在第二个参数:
canvas.getContext(contextType, contextAttributes)
复制代码
contextType
是上下文类型,通常值都是 2d
,除此以外还有 webgl
、webgl2
、bitmaprenderer
三个值,只不事后面三个浏览器支持度过低,通常不用
contextAttributes
是上下文属性,用于初始化上下文的一些属性,对于不一样的 contextType
,contextAttributes
的可取值也不一样,对于经常使用的 2d
,contextAttributes
可取值有:
boolean
类型值,代表 canvas
包含一个 alpha
通道. 默认为 true
,若是设置为 false
, 浏览器将认为 canvas
背景老是不透明的, 这样能够加速绘制透明的内容和图片
boolean
类型值,代表是否有重复读取计划。常用 getImageData()
,这将迫使软件使用 2D canvas
并节省内存(而不是硬件加速)。这个方案适用于存在属性 gfx.canvas.willReadFrequently
的环境。并设置为 true
(缺省状况下,只有B2G / Firefox OS
)
支持度低,目前只有 Gecko
内核的浏览器支持,不经常使用
string
这样表示使用哪一种方式存储(默认为:持久(persistent
))
支持度低,目前只有 Blink
内核的浏览器支持,不经常使用
上面三个属性,看经常使用的 alpha
就好了,若是你的游戏使用画布并且不须要透明,当使用 HTMLCanvasElement.getContext()
建立一个绘图上下文时把alpha
选项设置为 false
,这个选项能够帮助浏览器进行内部优化
const ctx = canvas.getContext('2d', { alpha: false })
复制代码
例如
shadow
相关 API
,此类 API
包括 shadowOffsetX
、shadowOffsetY
、shadowBlur
、shadowColor
绘图相关的 API
,例如 drawImage
、putImageData
,在绘制时进行缩放操做也会增长耗时时间
固然,上述都是尽可能避免 频繁调用,或用其余手段来控制性能,须要用到的地方确定仍是要用的
利用 canvas
进行动画绘制时,若是计算出来的坐标是浮点数,那么可能会出现 CSS Sub-pixel
的问题,也就是会自动将浮点数值四舍五入转为整数,那么在动画的过程当中,因为元素实际运动的轨迹并非严格按照计算公式获得,那么就可能出现抖动的状况,同时也可能让元素的边缘出现抗锯齿失真 这也是可能影响性能的一方面,由于一直在作没必要要的取证运算
渲染绘制的 api
,例如 stroke()
、fill
、drawImage
,都是将 ctx
状态机里面的状态真实绘制到画布上,这种操做也比较耗费性能
例如,若是你要绘制十条线段,那么先在 ctx
状态机中绘制出十天线段的状态机,再进行一次性的绘制,这将比每条线段都绘制一次要高效得多
for (let i = 0; i < 10; i++) {
context.beginPath()
context.moveTo(x1[i], y1[i])
context.lineTo(x2[i], y2[i])
// 每条线段都单独调用绘制操做,比较耗费性能
context.stroke()
}
for (let i = 0; i < 10; i++) {
context.beginPath()
context.moveTo(x1[i], y1[i])
context.lineTo(x2[i], y2[i])
}
// 先绘制一条包含多条线条的路径,最后再一次性绘制,能够获得更好的性能
context.stroke()
复制代码
ctx
能够看作是一个状态机,例如 fillStyle
、globalAlpha
、beginPath
,这些 api
都会改变 ctx
里面对于的状态,频繁改变状态机的状态,是影响性能的
能够经过对操做进行更好的规划,减小状态机的改变,从而获得更加的性能,例如在一个画布上绘制几行文字,最上面和最下面文字的字体都是 30px
,颜色都是 yellowgreen
,中间文字是 20px pink
,那么能够先绘制最上面和最下面的文字,再绘制中间的文字,而非必须从上往下依次绘制,由于前者减小了一次状态机的状态改变
const c = document.getElementById("myCanvas")
const ctx = c.getContext("2d")
ctx.font = '30 sans-serif'
ctx.fillStyle = 'yellowgreen'
ctx.fillText("你们好,我是最上面一行", 0, 40)
ctx.font = '20 sans-serif'
ctx.fillStyle = 'red'
ctx.fillText("你们好,我是中间一行", 0, 80)
ctx.font = '30 sans-serif'
ctx.fillStyle = 'yellowgreen'
ctx.fillText("你们好,我是最下面一行", 0, 130)
复制代码
下面的代码实现的效果和上面相同,可是代码量更少,同时比上述代码少改变了一次状态机,性能会更好
ctx.font = '30 sans-serif'
ctx.fillStyle = 'yellowgreen'
ctx.fillText("你们好,我是最上面一行", 0, 40)
ctx.fillText("你们好,我是最下面一行", 0, 130)
ctx.font = '20 sans-serif'
ctx.fillStyle = 'red'
ctx.fillText("你们好,我是中间一行", 0, 80)
复制代码
canvas API
嗯,canvas
也是经过操纵 js
来绘制的,可是相比于正常的 js
操做,调用 canvas API
将更加消耗资源,因此在绘制以前请作好规划,经过 适量 js
原生计算减小 canvas API
的调用是一件比较划算的事情
固然,请注意 适量二字,若是减小一行 canvas API
调用的代价是增长十行 js
计算,那这事可能就不必作了
在进行某些耗时操做,例如计算大量数据,一帧中包含了太多的绘制状态,大规模的 DOM
操做等,可能会致使页面卡顿,影响用户体验,能够经过如下两种手段:
web worker
最经常使用的场景就是大量的频繁计算,减轻主线程压力,若是遇到大规模的计算,能够经过此 API
分担主线程压力,此 API
兼容性已经很不错了,既然 canvas
能够用,那 web worker
也就彻底能够考虑使用
将一段大的任务过程分解成数个小型任务,使用定时器轮询进行,想要对一段任务进行分解操做,此任务须要知足如下状况:
分解任务包括两种情形:
例如进行一个千万级别的运算总任务,能够将其分解为 10
个百万级别的运算小任务
// 封装 定时器分解任务 函数
function processArray(items, process, callback) {
// 复制一份数组副本
var todo=items.concat();
setTimeout(function(){
process(todo.shift());
if(todo.length>0) {
// 将当前正在执行的函数自己再次使用定时器
setTimeout(arguments.callee, 25);
} else {
callback(items);
}
}, 25);
}
// 使用
var items=[12,34,65,2,4,76,235,24,9,90];
function outputValue(value) {
console.log(value);
}
processArray(items, outputValue, function(){
console.log('Done!');
});
复制代码
优势是任务分配模式比较简单,更有控制权,缺点是很差肯定小任务的大小
有的小任务可能由于某些缘由,会耗费比其余小任务更多的时间,这会形成线程阻塞;而有的小任务可能须要比其余任务少得多的时间,形成资源浪费
例如运行一个千万级别的运算总任务,不直接肯定分配为多少个子任务,或者分配的颗粒度比较小,在每个或几个计算完成后,查看此段运算消耗的时间,若是时间小于某个临界值,好比 10ms
,那么就继续进行运算,不然就暂停,等到下一个轮询再进行进行
function timedProcessArray(items, process, callback) {
var todo=items.concat();
setTimeout(function(){
// 开始计时
var start = +new Date();
// 若是单个数据处理时间小于 50ms ,则无需分解任务
do {
process(todo.shift());
} while (todo.length && (+new Date()-start < 50));
if(todo.length > 0) {
setTimeout(arguments.callee, 25);
} else {
callback(items);
}
});
}
复制代码
优势是避免了第一种状况出现的问题,缺点是多出了一个时间比较的运算,额外的运算过程也可能影响到性能
我准备作的 canvas
游戏彷佛须要的制做时间有点长,天天除了上班以外,剩下的时间实在是很少,不知道何时能搞完,若是一切顺利,我却是还想再用一些游戏引擎,例如 Egret
、LayaAir
、Cocos Creator
将其重制一遍,以熟悉这些游戏引擎的用法,而后到时候写个系列教程出来……
诶,这么看来,彷佛是要持久战了啊