隨著微信小游戲的推出索守,其全面支持以往的H5游戲開(kāi)發(fā),微信借小游戲的社交方式徹底激活小程序抑片。同樣的卵佛,也算是重新吹起了H5游戲的風(fēng)口。
可以預(yù)見(jiàn)的是,借助小游戲的風(fēng)截汪,前端游戲開(kāi)發(fā)這一分支也會(huì)燃起來(lái)了疾牲。在游戲開(kāi)發(fā)中,最令人難受的也許就是性能優(yōu)化了吧衙解。本人在整理過(guò)往幾次游戲開(kāi)發(fā)的經(jīng)歷中阳柔,總結(jié)了一些常被忽視的優(yōu)化小措施,與諸君分享蚓峦。
針對(duì)游戲性能優(yōu)化舌剂,首先,我們要知道我們優(yōu)化的目標(biāo)是什么暑椰?往往我們覺(jué)得性能優(yōu)化很難霍转,是因?yàn)槲覀儾淮_定優(yōu)化目標(biāo)是什么,針對(duì)什么進(jìn)行優(yōu)化一汽。
在我看來(lái)谴忧,性能優(yōu)化的實(shí)質(zhì),實(shí)際上就是盡可能的減少等待時(shí)間和內(nèi)存使用角虫。
有了目標(biāo)有好辦了沾谓,接下來(lái),我們需要知道我們通過(guò)優(yōu)化哪些目標(biāo)可以減少代碼執(zhí)行和內(nèi)存使用戳鹅。我粗略的分為了3個(gè)方面:
- Canvas均驶,原生canvas游戲,首要目標(biāo)自然就是canvas
- 內(nèi)存使用枫虏,果7之前safari運(yùn)行內(nèi)存只有100M妇穴,濫用內(nèi)存直接給你強(qiáng)制鎖死
- 多線程,兩條腿走路肯定比一條腿快多了啊
Canvas
離屏canvas
場(chǎng)景:針對(duì)需要大量使用且繪圖繁復(fù)的靜態(tài)場(chǎng)景
實(shí)現(xiàn):對(duì)象內(nèi)放置一個(gè)私有canvas隶债,初始化時(shí)將靜態(tài)場(chǎng)景繪制完備腾它,需要時(shí)直接拷貝內(nèi)置canvas的圖像即可
//每一幀重繪時(shí)
setInterval(function () {
context.fillRect(x,y,width,height)
context.arc(x,y,r,sA,eA)
context.strokeText('hehe', x, y)
}, 1000/60)
設(shè)置離屏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)
不設(shè)置離屏canvas的情況下,每幀繪制會(huì)調(diào)用3次繪圖api死讹;設(shè)置離屏canvas后瞒滴,每幀只用調(diào)用一次api。
實(shí)質(zhì):減少調(diào)用api的次數(shù)赞警,減少代碼執(zhí)行語(yǔ)句妓忍,從而減少每幀渲染時(shí)間,從而提高動(dòng)畫(huà)流程度愧旦。
狀態(tài)修改
場(chǎng)景:針對(duì)需要頻繁修改canvas對(duì)象的渲染狀態(tài) (fillStyle, strokeStyle ...)
實(shí)現(xiàn):按canvas狀態(tài)分別繪制世剖,而不是按對(duì)象進(jìn)行繪制
混合繪制
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)
}
不同狀態(tài)分別繪制:
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)
}
前后比較看,雖然循環(huán)次數(shù)沒(méi)變笤虫,但循環(huán)內(nèi)調(diào)用的語(yǔ)句變少了旁瘫,即不在循環(huán)內(nèi)修改canvas狀態(tài)了祖凫。
實(shí)質(zhì):減少canvas api的調(diào)用,不用在每次根據(jù)對(duì)象屬性去修改canvas的狀態(tài)酬凳,而是將具有相同狀態(tài)的對(duì)象提出蝙场,批量渲染。
分層和局部重繪
場(chǎng)景:針對(duì)場(chǎng)景中大背景變化緩慢粱年,而角色的狀態(tài)變換頻繁
實(shí)現(xiàn):將場(chǎng)景按狀態(tài)變換快慢進(jìn)行層次劃分售滤,設(shè)置不同的透明度和z-index進(jìn)行層級(jí)疊加。
實(shí)質(zhì):通過(guò)分層台诗,對(duì)連續(xù)幀中的相同場(chǎng)景不重復(fù)渲染完箩,減少渲染所需的canvas api的調(diào)用。
但在微信小游戲中拉队,本方法不能使用弊知,因?yàn)槲⑿判∮螒蛑杏腥治ㄒ籧anvas,其他canvas都是離屏canvas粱快,不能顯示秩彤。
requestAnimationFrame
這個(gè)不存在什么場(chǎng)景,就是一把梭事哭,無(wú)腦直接上RAF漫雷,別再setInterval了。
簡(jiǎn)單點(diǎn)說(shuō)鳍咱,RAF是瀏覽器根據(jù)頁(yè)面渲染的情況降盹,自行選擇下一幀繪制的時(shí)機(jī)。
但是有一個(gè)tip需要注意谤辜,RAF不管理回調(diào)函數(shù)蓄坏,即在RAF回調(diào)被執(zhí)行前,如果RAF多次調(diào)用丑念,其回調(diào)函數(shù)也會(huì)多次調(diào)用涡戳。所以需要做好防抖節(jié)流。不然會(huì)導(dǎo)致RAF的回調(diào)函數(shù)在同一幀中重復(fù)調(diào)用脯倚,造成不必要的計(jì)算和渲染的消耗渔彰。
const animation = timestamp => console.log('animation called at', timestamp)
window.requestAnimationFrame(animation)
window.requestAnimationFrame(animation)
內(nèi)存優(yōu)化
對(duì)象池
場(chǎng)景:針對(duì)游戲中需要頻繁更新和刪除 的角色
實(shí)現(xiàn):對(duì)象池維護(hù)一個(gè)裝著空閑對(duì)象的池子,如果需要對(duì)象的時(shí)候挠将,不是直接new胳岂,而是從對(duì)象池中取出编整,如果對(duì)象池中沒(méi)有空閑對(duì)象舔稀,則新建一個(gè)空閑對(duì)象。
const __ = {
poolDic: Symbol('poolDic')
}
/**
* 簡(jiǎn)易的對(duì)象池實(shí)現(xiàn)
* 用于對(duì)象的存貯和重復(fù)使用
* 可以有效減少對(duì)象創(chuàng)建開(kāi)銷和避免頻繁的垃圾回收
* 提高游戲性能
*/
export default class Pool {
constructor() {
this[__.poolDic] = {}
}
/**
* 根據(jù)對(duì)象標(biāo)識(shí)符
* 獲取對(duì)應(yīng)的對(duì)象池
*/
getPoolBySign(name) {
return this[__.poolDic][name] || ( this[__.poolDic][name] = [] )
}
/**
* 根據(jù)傳入的對(duì)象標(biāo)識(shí)符掌测,查詢對(duì)象池
* 對(duì)象池為空創(chuàng)建新的類内贮,否則從對(duì)象池中取
*/
getItemByClass(name, className) {
let pool = this.getPoolBySign(name)
let result = ( pool.length
? pool.shift()
: new Object() )
return result
}
/**
* 將對(duì)象回收到對(duì)象池
* 方便后續(xù)繼續(xù)使用
*/
recover(name, instance) {
this.getPoolBySign(name).push(instance)
}
}
實(shí)質(zhì):減少內(nèi)存的使用产园。每次創(chuàng)建一個(gè)對(duì)象,都需要分配一點(diǎn)內(nèi)存夜郁,而由于瀏覽器的回收機(jī)制什燕,導(dǎo)致會(huì)有大量無(wú)用的對(duì)象的累加,白白消耗大量的內(nèi)存竞端。
多線程
Worker
場(chǎng)景:針對(duì)需要進(jìn)行大量計(jì)算任務(wù)
實(shí)現(xiàn):使用worker單獨(dú)開(kāi)啟線程進(jìn)行并行計(jì)算屎即,主線程仍執(zhí)行自己的任務(wù)。
實(shí)質(zhì)就是并行計(jì)算事富,避免進(jìn)程堵塞技俐。任務(wù)計(jì)算需要的時(shí)間是不會(huì)減少的,形象點(diǎn)來(lái)說(shuō)就是從一條腿走路變成兩條腿走路
//main.js
//創(chuàng)建worker線程
let worker = new Worker('worker.js')
//監(jiān)聽(tīng)worker線程的返回事件
worker.onmessage = (e) => {
//e worker線程的返回對(duì)象
}
//發(fā)送消息
worker.postMessage(obj)
//worker.js
//監(jiān)聽(tīng)主線程的執(zhí)行請(qǐng)求
onmessage = (e) => {
//執(zhí)行對(duì)象e
postMessage(result)
}
實(shí)質(zhì):并行計(jì)算统台,可以認(rèn)為計(jì)算任務(wù)與主線程工作是異步的雕擂,互不干擾。因?yàn)槭菍⒂?jì)算任務(wù)全部交給worker贱勃,所有計(jì)算時(shí)間是不會(huì)減少的井赌。
對(duì)象池不僅可以針對(duì)對(duì)象,還可以針對(duì)worker進(jìn)行線程池的管理贵扰,有興趣的朋友可以試試仇穗。
其實(shí)除了上述3個(gè)方面,還有一個(gè)非常重要的優(yōu)化目標(biāo)戚绕,那就是網(wǎng)絡(luò)優(yōu)化仪缸,但這也是我們常說(shuō)的瀏覽器性能優(yōu)化的終點(diǎn)內(nèi)容,所以關(guān)于網(wǎng)絡(luò)優(yōu)化列肢,各位就請(qǐng)移步其他大神的文章恰画,我也就不再賣弄我那一點(diǎn)三腳貓技術(shù)了。各位朋友有什么其他的優(yōu)化措施的瓷马,歡迎交流拴还。