最近對 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
將其重製一遍,以熟悉這些遊戲引擎的用法,而後到時候寫個系列教程出來……
誒,這麼看來,彷佛是要持久戰了啊