原生canvas游戏性能优化

Posted 三水清

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了原生canvas游戏性能优化相关的知识,希望对你有一定的参考价值。

随着微信小游戏的推出,其全面支持以往的 H5 游戏开发,微信借小游戏的社交方式彻底激活小程序。同样的,也算是重新吹起了 H5 游戏的风口。

可以预见的是,借助小游戏的风,前端游戏开发这一分支也会燃起来了。在游戏开发中,最令人难受的也许就是性能优化了吧。本人在整理过往几次游戏开发的经历中,总结了一些常被忽视的优化小措施,与诸君分享。


针对游戏性能优化,首先,我们要知道我们优化的目标是什么?往往我们觉得性能优化很难,是因为我们不确定优化目标是什么,针对什么进行优化。

在我看来,性能优化的实质,实际上就是尽可能的减少等待时间和内存使用。

有了目标有好办了,接下来,我们需要知道我们通过优化哪些目标可以减少代码执行和内存使用。我粗略的分为了 3 个方面:

  • Canvas,原生 canvas 游戏,首要目标自然就是 canvas

  • 内存使用,果 7 之前 safari 运行内存只有 100M,滥用内存直接给你强制锁死

  • 多线程,两条腿走路肯定比一条腿快多了啊

Canvas

离屏 canvas

场景:针对需要大量使用且绘图繁复的静态场景

实现:对象内放置一个私有 canvas,初始化时将静态场景绘制完备,需要时直接拷贝内置 canvas 的图像即可

 
   
   
 
  1. //每一帧重绘时

  2. setInterval(function () {

  3.    context.fillRect(x,y,width,height)

  4.    context.arc(x,y,r,sA,eA)

  5.    context.strokeText('hehe', x, y)

  6. }, 1000/60)

设置离屏 canvas

 
   
   
 
  1. let background = {

  2.    width: 400,

  3.    height: 400,

  4.    canvas: document.createElement('canvas'),

  5.    init: () => {

  6.        let self = this

  7.        let ctx = self.canvas.getContext('2d')

  8.        self.canvas.width = self.width

  9.        self.canvas.height = self.height

  10.        ctx.fillRect(x,y,width,height)

  11.        ctx.arc(x,y,r,sA,eA)

  12.        ctx.strokeText('hehe', x, y)

  13.    }

  14. }

  15. background.init()

  16. setInterval(() => {

  17.    context.drawImage(background.canvas, background.width, background.height, 0, 0);

  18. }, 1000/60)

不设置离屏 canvas 的情况下,每帧绘制会调用 3 次绘图 api;设置离屏 canvas 后,每帧只用调用一次 api。

实质:减少调用 api 的次数,减少代码执行语句,从而减少每帧渲染时间,从而提高动画流程度。

状态修改

场景:针对需要频繁修改 canvas 对象的渲染状态 (fillStyle, strokeStyle ...)

实现:按 canvas 状态分别绘制,而不是按对象进行绘制

混合绘制

 
   
   
 
  1. for (let i = 0; i < line.length; i++) {

  2.    let e = line[i]

  3.    context.fillStyle = i % 2 ? '#000': '#fff'

  4.    context.fillRect(e.x, e.y, e.width, e.height)

  5. }

不同状态分别绘制:

 
   
   
 
  1. context.fillStyle = '#000'

  2. for (let i = 0; i < line.length / 2 - 1; i++) {

  3.    let e = line[i * 2 + 1]

  4.    context.fillRect(e.x, e.y, e.width, e.height)

  5. }

  6. context.fillStyle = '#fff'

  7. for (let i = 0; i < line.length / 2 - 1; i++) {

  8.    let e = line[i * 2]

  9.    context.fillRect(e.x, e.y, e.width, e.height)

  10. }

前后比较看,虽然循环次数没变,但循环内调用的语句变少了,即不在循环内修改 canvas 状态了。

实质:减少 canvas api 的调用,不用在每次根据对象属性去修改 canvas 的状态,而是将具有相同状态的对象提出,批量渲染。

分层和局部重绘

场景:针对场景中大背景变化缓慢,而角色的状态变换频繁

实现:将场景按状态变换快慢进行层次划分,设置不同的透明度和 z-index 进行层级叠加。

实质:通过分层,对连续帧中的相同场景不重复渲染,减少渲染所需的 canvas api 的调用。

但在微信小游戏中,本方法不能使用,因为微信小游戏中有全局唯一 canvas,其他 canvas 都是离屏 canvas,不能显示。

requestAnimationFrame

这个不存在什么场景,就是一把梭,无脑直接上 RAF,别再 setInterval 了。

简单点说,RAF 是浏览器根据页面渲染的情况,自行选择下一帧绘制的时机。

但是有一个 tip 需要注意,RAF 不管理回调函数,即在 RAF 回调被执行前,如果 RAF 多次调用,其回调函数也会多次调用。所以需要做好防抖节流。不然会导致 RAF 的回调函数在同一帧中重复调用,造成不必要的计算和渲染的消耗。

 
   
   
 
  1. const animation = timestamp => console.log('animation called at', timestamp)

  2. window.requestAnimationFrame(animation)

  3. window.requestAnimationFrame(animation)


内存优化

对象池

场景:针对游戏中需要频繁更新和删除 的角色

实现:对象池维护一个装着空闲对象的池子,如果需要对象的时候,不是直接 new,而是从对象池中取出,如果对象池中没有空闲对象,则新建一个空闲对象。

 
   
   
 
  1. const __ = {

  2.  poolDic: Symbol('poolDic')

  3. }

  4. /**

  5. * 简易的对象池实现

  6. * 用于对象的存贮和重复使用

  7. * 可以有效减少对象创建开销和避免频繁的垃圾回收

  8. * 提高游戏性能

  9. */

  10. export default class Pool {

  11.  constructor() {

  12.    this[__.poolDic] = {}

  13.  }

  14.  /**

  15.   * 根据对象标识符

  16.   * 获取对应的对象池

  17.   */

  18.  getPoolBySign(name) {

  19.    return this[__.poolDic][name] || ( this[__.poolDic][name] = [] )

  20.  }

  21.  /**

  22.   * 根据传入的对象标识符,查询对象池

  23.   * 对象池为空创建新的类,否则从对象池中取

  24.   */

  25.  getItemByClass(name, className) {

  26.    let pool = this.getPoolBySign(name)

  27.    let result = (  pool.length

  28.                  ? pool.shift()

  29.                  : new Object()  )

  30.    return result

  31.  }

  32.  /**

  33.   * 将对象回收到对象池

  34.   * 方便后续继续使用

  35.   */

  36.  recover(name, instance) {

  37.    this.getPoolBySign(name).push(instance)

  38.  }

  39. }

实质:减少内存的使用。每次创建一个对象,都需要分配一点内存,而由于浏览器的回收机制,导致会有大量无用的对象的累加,白白消耗大量的内存。


多线程

Worker

场景:针对需要进行大量计算任务

实现:使用 worker 单独开启线程进行并行计算,主线程仍执行自己的任务。

实质就是并行计算,避免进程堵塞。任务计算需要的时间是不会减少的,形象点来说就是从一条腿走路变成两条腿走路

 
   
   
 
  1. //main.js

  2. //创建worker线程

  3. let worker = new Worker('worker.js')

  4. //监听worker线程的返回事件

  5. worker.onmessage = (e) => {

  6.    //e worker线程的返回对象

  7. }

  8. //发送消息

  9. worker.postMessage(obj)

  10. //worker.js

  11. //监听主线程的执行请求

  12. onmessage = (e) => {

  13.    //执行对象e

  14.    postMessage(result)

  15. }

实质:并行计算,可以认为计算任务与主线程工作是异步的,互不干扰。因为是将计算任务全部交给 worker,所有计算时间是不会减少的。

对象池不仅可以针对对象,还可以针对 worker 进行线程池的管理,有兴趣的朋友可以试试。


其实除了上述 3 个方面,还有一个非常重要的优化目标,那就是网络优化,但这也是我们常说的浏览器性能优化的终点内容,所以关于网络优化,各位就请移步其他大神的文章,我也就不再卖弄我那一点三脚猫技术了。各位朋友有什么其他的优化措施的,欢迎交流。


以上是关于原生canvas游戏性能优化的主要内容,如果未能解决你的问题,请参考以下文章

原生JS+Canvas实现五子棋游戏

原生 JS + Canvas 实现五子棋游戏

前端小游戏页面性能优化

原生 JS + Canvas 实现五子棋游戏

如何针对游戏性能优化Windows 10

html5 canvas 通用性能提示