在游戲中發(fā)揮HTML5 Canvas的潛能
在游戲中,fluidity(流暢性)這一概念非常重要,因?yàn)楹玫牧鲿承阅軒Ыo玩家更好的體驗(yàn)。
這篇文章的主旨在于教你一些技巧,讓你可以最大程度地發(fā)揮HTML5 canvas的潛能。
我將通過一個(gè)例子來表達(dá)我要講的內(nèi)容。這個(gè)例子是2D tunnel effect(2D隧道效應(yīng)),它是我為Coding4Fun 會(huì)議寫的(我參加了在法國舉辦的TechDays 2012)。
(http://video.fr.msn.com/watch/video/techdays-2012-session-technique-coding4fun/zqy7cm8l).
早在80年代,當(dāng)我還是一個(gè)年輕的demomaker的時(shí)候曾受到一些Commodore AMIGA代碼的啟發(fā),從而寫了這個(gè)2D tunnel effect。
經(jīng)過不斷地改進(jìn),現(xiàn)在它僅使用了canvas和Javascript(最初的代碼是基于68000匯編的):
完整的代碼可以從這里獲得:http://www.catuhe.com/msdn/canvas/tunnel.zip
這篇文章的目的不是講解該程序的開發(fā)過程,而是讓你通過優(yōu)化已有的代碼來達(dá)到一個(gè)實(shí)時(shí)性的效果。
使用off-screen canvas(離屏畫布)來讀取圖片數(shù)據(jù)
我想講的第一點(diǎn)是怎樣使用canvas來讀取圖片數(shù)據(jù)。實(shí)際上,任何一個(gè)游戲都需要圖形來顯示游戲界面和背景。canvas有一個(gè)非常有用的畫圖方法:drawImage 。這個(gè)功能可以用來繪制游戲界面,通過它你可以定義起始和目的區(qū)域。
但是有時(shí)候光使用它是不夠的,比如你想要在源圖像上實(shí)現(xiàn)一些特效,或者源圖像不是一個(gè)簡單的位圖而是一個(gè)復(fù)雜的資源(如地圖)。
在這些情況下,你需要訪問到圖片的內(nèi)部數(shù)據(jù)。但是Image標(biāo)簽無法讀到這些數(shù)據(jù),這時(shí)候就該canvas上場(chǎng)了。
事實(shí)上,每當(dāng)你需要從圖片中讀取內(nèi)容的時(shí)候,你都可以使用off-screen canvas。也就是說,當(dāng)你導(dǎo)入一張圖片的時(shí)候,你只需要將它渲染到canvas中(而不是DOM里),然后你就可以通過讀取canvas的像素點(diǎn)來獲得源圖片的內(nèi)容了(這個(gè)過程非常簡單)。
有關(guān)部分的代碼如下(2D tunnel effect中用來讀取隧道的紋理數(shù)據(jù)的):
- var loadTexture = function (name, then) {
- var texture = new Image();
- var textureData;
- var textureWidth;
- var textureHeight;
- var result = {};
- // on load
- texture.addEventListener(‘load’, function () {
- var textureCanvas = document.createElement(‘canvas’); // off-screen canvas
- // Setting the canvas to right size
- textureCanvas.width = this.width; //<– “this” is the image
- textureCanvas.height = this.height;
- result.width = this.width;
- result.height = this.height;
- var textureContext = textureCanvas.getContext(’2d’);
- textureContext.drawImage(this, 0, 0);
- result.data = textureContext.getImageData(0, 0, this.width, this.height).data;
- then();
- }, false);
- // Loading
- texture.src = name;
- return result;
- };
為了使用這些代碼,你還要保證隧道紋理圖片的導(dǎo)入是異步的,因此你需要傳遞then參數(shù),代碼如下:
- // Texture
- var texture = loadTexture(“soft.png”, function () {
- // Launching the render
- QueueNewFrame();
- });
使用硬件縮放功能
現(xiàn)代瀏覽器和Windows8都支持硬件加速的canvas,這意味著,你可以使用GPU來調(diào)整canvas里面內(nèi)容的尺寸。
在2D tunnel effect里,該算法要求處理canvas的每一個(gè)像素點(diǎn),因此一個(gè)1024×768的canvas就得處理786432個(gè)像素點(diǎn),并且為了達(dá)到流暢性的要求,每分鐘得處理60次,也就是說每分鐘要處理47185920個(gè)像素點(diǎn)!
很顯然,任何可以減少像素處理總數(shù)的方法都能帶來極大的性能提升。
又一次輪到canvas上場(chǎng)了!下面的代碼展示了怎樣使用硬件加速來調(diào)整canvas的內(nèi)部有效區(qū)域使之等于DOM對(duì)象的外部尺寸:
- // Setting hardware scaling
- canvas.width = 300;
- canvas.style.width = window.innerWidth + ‘px’;
- canvas.height = 200;
- canvas.style.height = window.innerHeight + ‘px’;
請(qǐng)注意DOM對(duì)象的尺寸(canvas.style.width、canvas.style.height)和canvas有效區(qū)域的尺寸(canvas.width、canvas.height)之間的差別。
當(dāng)這兩個(gè)尺寸不同的時(shí)候,硬件會(huì)自動(dòng)調(diào)整有效區(qū)域的大小,這是一件很棒的事:我們可以繪制低分辨率的圖形,然后通過GPU的調(diào)整使之符合DOM對(duì)象的大?。▽?shí)現(xiàn)一個(gè)漂亮免費(fèi)的模糊濾鏡效果)。
在這種情況下,本來只有300×200 的圖像會(huì)被GPU擴(kuò)展到跟你的窗口一樣大。
所有的現(xiàn)代瀏覽器都支持該功能,因此你可以放心的使用。
優(yōu)化rendering loop
制作游戲的時(shí)候,需要一個(gè)rendering loop用來繪制所有的組件(如背景,界面,分?jǐn)?shù)等等)。這個(gè)loop是代碼的核心,因此必須充分優(yōu)化從而保證游戲的快速和流暢。
RequestAnimationFrame
HTML5一個(gè)有趣的功能是使用window.requestAnimationFrame. 代替window.setInterval 來創(chuàng)建定時(shí)器,從而實(shí)現(xiàn)每(1000/16) 毫秒渲染一次(以達(dá)到60fps),你可以通過requestAnimationFrame將該任務(wù)交給瀏覽器。調(diào)用這個(gè)方法表明你想要盡快的更新有關(guān)的圖形。
瀏覽器會(huì)將你的請(qǐng)求放入內(nèi)部渲染計(jì)劃,并使之與其本身的渲染及動(dòng)畫代碼(CSS, transitions等等)同步。這個(gè)方法另一個(gè)有趣的地方在于如果窗口不顯示(minimized, fully occluded等等),你的代碼就不會(huì)被調(diào)用。
這能改善性能,因?yàn)闉g覽器可以優(yōu)化并發(fā)渲染從而提高動(dòng)畫的流暢性(如你的渲染周期太長的話瀏覽器會(huì)將它與其本身的渲染及動(dòng)畫周期同步)。
代碼很清晰(別忘了window前綴):
- var intervalID = -1;
- var QueueNewFrame = function () {
- if (window.requestAnimationFrame)
- window.requestAnimationFrame(renderingLoop);
- else if (window.msRequestAnimationFrame)
- window.msRequestAnimationFrame(renderingLoop);
- else if (window.webkitRequestAnimationFrame)
- window.webkitRequestAnimationFrame(renderingLoop);
- else if (window.mozRequestAnimationFrame)
- window.mozRequestAnimationFrame(renderingLoop);
- else if (window.oRequestAnimationFrame)
- window.oRequestAnimationFrame(renderingLoop);
- else {
- QueueNewFrame = function () {
- };
- intervalID = window.setInterval(renderingLoop, 16.7);
- }
- };
你只需要在rendering loop的結(jié)尾調(diào)用這個(gè)函數(shù)并在接下來的代碼段里進(jìn)行注冊(cè)即可:
- var renderingLoop = function () {
- …
- QueueNewFrame();
- };
訪問DOM(Document Object Model)
為了優(yōu)化rendering loop,你必須遵循這條黃金準(zhǔn)則:DO NOT ACCESS THE DOM(不要訪問DOM)。即使現(xiàn)代瀏覽器在這方面做了優(yōu)化,讀取DOM對(duì)象屬性還是太慢了。
例如,在我的代碼里,我使用了Internet Explorer 10 profiler(IE10的提供的分析器,按F12快捷鍵打開),顯示的結(jié)果如下:

如圖所示,訪問canvas的寬度和高度會(huì)花費(fèi)大量的時(shí)間!
原始代碼如下:
- var renderingLoop = function () {
- for (var y = -canvas.height / 2; y < canvas.height / 2; y++) {
- for (var x = -canvas.width / 2; x < canvas.width / 2; x++) {
- …
- }
- }
- };
你可以通過兩個(gè)變量來預(yù)先獲取canvas.width 和 canvas.height,然后在后面用變量來代替這些屬性值:
- var renderingLoop = function () {
- var index = 0;
- for (var y = -canvasHeight / 2; y < canvasHeight / 2; y++) {
- for (var x = -canvasWidth / 2; x < canvasWidth / 2; x++) {
- …
- }
- }
- };
是不是非常簡單?雖然有時(shí)候很難注意到這些細(xì)節(jié),但是請(qǐng)相信我這絕對(duì)是件值得的事。
預(yù)先計(jì)算
通過分析器得知,Math.atan2函數(shù)比較慢。事實(shí)上,該操作并不需要在運(yùn)行時(shí)計(jì)算,你可以在JavaScript里面加一些代碼預(yù)先計(jì)算出結(jié)果。

一般來說,預(yù)先計(jì)算一些較為費(fèi)時(shí)的代碼是一種好方法。這里,在運(yùn)行rendering loop之前,我已經(jīng)計(jì)算好了Math.atan2:
- // precompute arctangent
- var atans = [];
- var index = 0;
- for (var y = -canvasHeight / 2; y < canvasHeight / 2; y++) {
- for (var x = -canvasWidth / 2; x < canvasWidth / 2; x++) {
- atans[index++] = Math.atan2(y, x) / Math.PI;
- }
- }
atans數(shù)組的使用明顯的提高了性能。
避免使用Math.round, Math.floor 以及 parseInt
最后一點(diǎn)是parseInt的使用:

當(dāng)你使用canvas時(shí),你需要使用一些整數(shù)坐標(biāo)。實(shí)際上,所有的計(jì)算都采用的浮點(diǎn)數(shù),你需要將它們轉(zhuǎn)換成整形。
JavaScript 提供了 Math.round, Math.floor 甚至 parseInt 來轉(zhuǎn)換數(shù)值。但是這個(gè)方法做了一些額外的工作(比如檢測(cè)數(shù)據(jù)是不是有效的數(shù)值,parseInt 甚至先將參數(shù)轉(zhuǎn)換成了字符串!)。在我的rendering loop里面,我需要一個(gè)更快的轉(zhuǎn)換方法。
在我的舊的匯編代碼里面,我使用了一個(gè)小技巧:將數(shù)據(jù)右移0位。這會(huì)將浮點(diǎn)數(shù)從浮點(diǎn)寄存器移到整數(shù)寄存器,并且是通過硬件轉(zhuǎn)換的。右移0位不會(huì)改變數(shù)據(jù)的值,但是會(huì)以整數(shù)形式返回。
原始代碼如下:
u = parseInt((u < 0) ? texture.width + (u % texture.width) : (u >= texture.width) ? u % texture.width : u);
以下是改進(jìn)后的代碼:
u = ((u < 0) ? texture.width + (u % texture.width) : (u >= texture.width) ? u % texture.width : u)>> 0;
當(dāng)然該方法要求你的數(shù)據(jù)是合法的數(shù)值。
最終結(jié)果
實(shí)現(xiàn)了上述優(yōu)化之后得到的結(jié)果如下:

你可以看到做了這些基本的功能優(yōu)化之后的表現(xiàn)。
原始的隧道渲染(沒做任何優(yōu)化):
做完上述優(yōu)化之后:
下表展示了每項(xiàng)優(yōu)化對(duì)幀速率的影響(在我的機(jī)器上):

更進(jìn)一步
記住這些關(guān)鍵技巧,你就可以為現(xiàn)代瀏覽器或Windows8制作實(shí)時(shí)、快速、流暢的游戲了。























