原生Canvas游戏性能优化

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

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


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

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

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

  • Canvas,原生canvas游戏,首要目标自然就是canvas
  • 内存使用,果7之前safari运行内存只有100M,滥用内存直接给你强制锁死
  • 多线程,两条腿走路肯定比一条腿快多了啊

Canvas

离屏canvas

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

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

//每一帧重绘时
setInterval(function () {

    context.fillRect(x,y,width,height)
    context.arc(x,y,r,sA,eA)
    context.strokeText('hehe', x, y)
    
}, 1000/60)

设置离屏canvas

let background = {
    width: 400,
    height: 400,
    canvas: document.createElement('canvas'),
    init: () => {
        let self = this
        let ctx = self.canvas.getContext('2d')
        
        self.canvas.width = self.width
        self.canvas.height = self.height

        ctx.fillRect(x,y,width,height)
        ctx.arc(x,y,r,sA,eA)
        ctx.strokeText('hehe', x, y)

    }
    
}

background.init()

setInterval(() => {

    context.drawImage(background.canvas, background.width, background.height, 0, 0);
    
}, 1000/60)

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

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

状态修改

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

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

混合绘制


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

    let e = line[i]

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

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

}

不同状态分别绘制:


context.fillStyle = '#000'

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

    let e = line[i * 2 + 1]

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

}

context.fillStyle = '#fff'

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

    let e = line[i * 2]
    
    context.fillRect(e.x, e.y, e.width, e.height)

}

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

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

分层和局部重绘

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

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

分层.jpg

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

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

requestAnimationFrame

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

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

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

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

window.requestAnimationFrame(animation)
window.requestAnimationFrame(animation)

内存优化

对象池

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

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

const __ = {
  poolDic: Symbol('poolDic')
}

/**
 * 简易的对象池实现
 * 用于对象的存贮和重复使用
 * 可以有效减少对象创建开销和避免频繁的垃圾回收
 * 提高游戏性能
 */
export default class Pool {
  constructor() {
    this[__.poolDic] = {}
  }

  /**
   * 根据对象标识符
   * 获取对应的对象池
   */
  getPoolBySign(name) {
    return this[__.poolDic][name] || ( this[__.poolDic][name] = [] )
  }

  /**
   * 根据传入的对象标识符,查询对象池
   * 对象池为空创建新的类,否则从对象池中取
   */
  getItemByClass(name, className) {
    let pool = this.getPoolBySign(name)

    let result = (  pool.length
                  ? pool.shift()
                  : new Object()  )

    return result
  }

  /**
   * 将对象回收到对象池
   * 方便后续继续使用
   */
  recover(name, instance) {
    this.getPoolBySign(name).push(instance)
  }
}

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


多线程

Worker

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

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

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

//main.js
//创建worker线程
let worker = new Worker('worker.js')
//监听worker线程的返回事件
worker.onmessage = (e) => {
    //e worker线程的返回对象
}
//发送消息
worker.postMessage(obj)

//worker.js
//监听主线程的执行请求
onmessage = (e) => {
    //执行对象e
    
    postMessage(result)
}

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

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


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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351

推荐阅读更多精彩内容