隨著微信小遊戲的推出,其全面支援以往的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進行層級疊加。
實質:通過分層,對連續幀中的相同場景不重複渲染,減少渲染所需的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個方面,還有一個非常重要的優化目標,那就是網路優化,但這也是我們常說的瀏覽器效能優化的終點內容,所以關於網路優化,各位就請移步其他大神的文章,我也就不再賣弄我那一點三腳貓技術了。各位朋友有什麼其他的優化措施的,歡迎交流。