當前位置:首頁 >教程首頁 > 华体会hth体育app在线登录 > 遊戲動作設計師班 >如何用HTML5 Canvas製作子畫麵動畫

如何用HTML5 Canvas製作子畫麵動畫

發布時間:2018-11-17 19:57:24
作者:Martin Wells
 
子麵畫基本原理
 
我一直很喜歡網頁遊戲,因為大多數都容易製作,而且容易玩(隻要點擊一個鏈接就可以開始玩了)。

Ajax和移動DOM元素是有些意思,但製約了你能製作的遊戲類型。對於遊戲開發者,技術不僅一直在變化,而且是飛速變化。HTML5為網頁遊戲開發不斷地提供大量新選擇,瀏覽器供應商也為成為新標準的最佳平台而展開激烈競爭。

sprite-animations
 
所以,從遊戲開發者的角度看,一切都朝著正確的方向發展:2D和3D硬件運算速度越來越快、javascript引擎的表現性能越來越好、排錯和分析工具高度集成,以及可能最重要的,瀏覽器供應商正在積極地角逐最佳網頁遊戲平台。
 
所以工具實用了,瀏覽器強大了,供應商重視了,我們就可以製作出優秀的遊戲了,對吧?基本上HTML5/Javascript遊戲開發仍然處於發展初期,會遇到許多誤區和技術選擇。
 
在本文中,我將介紹一些開發2D遊戲的選擇,但願能讓讀者對開發HTML5遊戲有所了解。
 
基礎
 
你要回答的第一個問題是,是使用HTML5 Canvas來繪製圖像(場景圖像)還是通過修改DOM元素。
 
為了用DOM做2D遊戲,你基本上要動態地調整元素風格,以便在頁麵上移動它。雖然有些時候DOM修改是很好的,但這一次我將重點介紹使用HTML5 Canvas來製作圖像,因為對於現代瀏覽器,它是最靈活的。

頁麵設置
 
首先,你要創建一個HTML頁麵,其中包含如下canvas標簽:

<!doctype html>
 <html>
 <head>
 <title></title>
 </head>
 <body style=’position: absolute; padding:0; margin:0; height: 100%; width:100%’>
 <canvas id=”gameCanvas”></canvas>
 </body>
 </html>
 
如果你載入以上代碼,當然什麼也不會出現。那是因為雖然我們有一個canvas標簽,但我們還沒在上麵繪製任何東西。我們來添加一些簡單的canvas命令來繪製小箱子吧。

<head>
 <title></title>
 <script type=’text/javascript’>
 var canvas = null;
 function onload() {
 canvas = document.getElementById(‘gameCanvas’);
 var ctx = canvas.getContext(“2d”);
 ctx.fillStyle = ‘#000000′;
 ctx.fillRect(0, 0, canvas.width, canvas.height);
 ctx.fillStyle = ‘#333333′;
 ctx.fillRect(canvas.width / 3, canvas.height / 3, canvas.width / 3,
 canvas.height / 3);
 }
 </script>
 </head>
 <body onload=’onload()’ …
 
在這個例子中,我已經在body標簽中添加了一個onload事件,然後執行功能獲得畫布元素,並繪製幾個箱子。非常簡單。
 

result 1
 
這個箱子不錯,但你會注意到,畫布沒有鋪滿整個瀏覽器窗口。為了解決這個問題,我們可以增加畫布的寬度和高度。我是指根據畫布所包含的文件元素的大小來靈活地調整畫布尺寸。

var canvas = null;
 function onload() {
 canvas = document.getElementById(‘gameCanvas’);
 canvas.width = canvas.parentNode.clientWidth;
 canvas.height = canvas.parentNode.clientHeight;
 …
 
加載後,你會看到畫布鋪滿整個屏幕了。太好了。
 
再進一步,如果瀏覽器窗口大小是由用戶調整的,我們還要重置畫布的尺寸。

var canvas = null;
 function onload() {
 canvas = document.getElementById(‘gameCanvas’);
 resize();
 }
 function resize() {
 canvas.width = canvas.parentNode.clientWidth;
 canvas.height = canvas.parentNode.clientHeight;
 var ctx = canvas.getContext(“2d”);
 ctx.fillStyle = ‘#000000′;
 ctx.fillRect(0, 0, canvas.width, canvas.height);
 ctx.fillStyle = ‘#333333′;
 ctx.fillRect(canvas.width/3, canvas.height/3, canvas.width/3, canvas.height/3);
 }
 
添加onresize命令到body標簽。

<body onresize=’resize()’ …
 
現在,如果你調整瀏覽器的大小,矩形應該如下圖所示。
 

result 2
 
載入圖像
 
大部分遊戲都需要動畫的子畫麵,所以我來添加一些圖像吧。
 
首先,你需要圖像資源。因為我們要用javascript繪製它,所以我覺得先聲明圖像然後設置它的src屬性為你想載入的圖像的URL,比較合理。

var img = null;
 function onload() {
 …
 img = new Image();
 img.src = ‘simba.png’;
 }
 
然後你可以通過添加這個到resize方法中來繪製圖像:

ctx.drawImage(img, canvas.width/2 – (img.width/2), canvas.height/2 – (img.height/2));
 
如果你重新載入頁麵後,在大部分情況下,你會看到圖像出現了。不過我說的是大部分情況下,因為這取決於你的機器跑得有多快、瀏覽器是否已經緩存了圖像。那是因為resize方法的調用時間介於你開始載入圖像(設置它的src屬性)的時間到瀏覽器準備好的時間之間。對於一兩張圖像,這個方法可能不錯,但當你的遊戲開始變大時,你就必須等到所有圖像加載完才能執行活動。
 
給圖像添加一個通知監聽器,這樣當圖像準備就緒時你就會收到回叫信號。我得重新整理一下,以下是更新過的代碼:

var canvas = null;
 var img = null; var ctx = null;
 var imageReady = false;
 function onload() {
 canvas = document.getElementById(‘gameCanvas’);
 ctx = canvas.getContext(“2d”);
 img = new Image();
 img.src = ‘images/simba.png’;
 img.onload = loaded();
 resize();
 }
 function loaded() {
 imageReady = true; redraw();
 }
 function resize() {
 canvas.width = canvas.parentNode.clientWidth;
 canvas.height = canvas.parentNode.clientHeight; redraw();
 }
 function redraw() {
 ctx.fillStyle = ‘#000000′;
 ctx.fillRect(0, 0, canvas.width, canvas.height);
 if (imageReady)
 ctx.drawImage(img, canvas.width/2 – (img.width/2), canvas.height/2 – (img.height/2));
 }
 
結果應該是:

result 3

這個圖像顯示了一隻吸血鬼貓(好吧,是我自己覺得像)的6個奔跑幀。為了把這個子畫麵做成動畫,我們必須每次繪製一個幀。

子畫麵動畫

你可以用drawImage命令的源參數繪製一個幀。事實上,是隻繪製源圖像的一部分。所以為了繪製這唯一的第一幀,使用允許你指定源圖像中的矩形的drawImage的拓展版。因為我們的貓動畫是由6個96 x 96象素大小的幀組成的,我們可以添加:

ctx.drawImage(img, 0, 0, 96, 54, canvas.width/2 – 48, canvas.height/2 – 48, 96, 54);

這裏的關鍵是起點(0, 0, 96, 54)。這限製被繪製圖像為貓動畫的第一幀。我還設置根據單幀來居中,而不是包含所有6幀的整個圖像尺寸。

現在總算有點意思了。為了讓圖像動起來,我們必須追蹤要繪製的幀,然後隨著時間推進幀數。為此,我們必須把靜止頁麵做成隔時循環的頁麵。

我們按照老方法來做。添加60幀每秒間隔計時器。為了保證隻有圖像加載後才開始循環動畫,我們要在loaded功能中添加以下命令:

function loaded() {
 imageReady = true;
 setTimeout( update, 1000 / 60 );
 }

添加更新後的函數,然後調用redraw:

var frame = 0;
 function update() {
 redraw(); frame++;
 if (frame >= 6) frame = 0;
 setTimeout( update, 1000 / 60 );
 }
 
當繪製後且幀推進完,計時器就會重置。
 
下一步,調整繪製圖像,使源窗口根據我們想要繪製的那一幀位置來移動(關鍵是給幀設置的源X位置,是幀乘上幀的大小)。

function redraw() {
 ctx.fillStyle = ‘#000000′;
 ctx.fillRect(0, 0, canvas.width, canvas.height);
 if (imageReady)
 ctx.drawImage(img, frame*96, 0, 96, 54,
 canvas.width/2 – 48, canvas.height/2 – 48, 96, 54);
 }

結果如下:

result 4

我們邪惡的不活吸血貓活了!跑得太快了。

我們還要對動畫做一些改進。

requestAnimFrame

setTimeout很好,幾乎在所有瀏覽器上都運行得不錯,但還有一個更好的方法,那就是requestAnimFrame。

requestAnimFrame的作用基本上就是setTimeout,但瀏覽器知道你正在渲染幀,所以它可以優化繪製循環,以及如何與剩下的頁麵回流。它甚至會檢測標簽是否可見,如果隱藏就不繪製,這樣就節省了電池(是的,以60fps的速率循環的網頁遊戲是很燒電池的)。另外,瀏覽器還有機會以其他我們不知道的方式進行優化。根據我對更高級的幀加載的經驗,這樣可以大大提高表現,特別是在現在的瀏覽器中。
 
我要給讀者提個醒,在某些情況下,setTimeout比requestAnimFrame更好用,特別是對於手機。測試一下,根據設備配置一下你的應用。
 
在不同的瀏覽器上調用requestAnimFrame的情況也不同,標準的檢測方法如下:

window.requestAnimFrame = (function(){
 return window.requestAnimationFrame ||
 window.webkitRequestAnimationFrame ||
 window.mozRequestAnimationFrame ||
 window.oRequestAnimationFrame ||
 window.msRequestAnimationFrame ||
 function( callback ){
 window.setTimeout(callback, 1000 / 60);
 };
 })();
 
如果requestAnimFrame支持不可用,還是可以用回內置的setTimeout。
 
然後你必須修改update方法,以便重複獲得請求:

function update() {
 requestAnimFrame(update);
 redraw();
 frame++;
 if (frame >= 6) frame = 0;
 }
 
在渲染/更新以前調用requestAnimFrame,往往能獲得更連貫的效果。
 
另外,當我第一次使用requestAnimFrame時,我試圖查找它如何計時的資料,但什麼也沒找到。那是因為它本來就是不能計時的。setTimeout沒有什麼與設置MS延時相當的東西,這意味著你不可能控製幀率。那你就做好你該做的事,其他的就讓瀏覽器去處理吧。
 
另一件要注意的事是,如果你封閉使用requestAnimFrame,那麼你必須做一個本地交換來調用它,如:

my.requestAnimFrame = (function () {
 var func = window.requestAnimationFrame ||
 window.webkitRequestAnimationFrame ||
 window.mozRequestAnimationFrame ||
 window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
 function (callback, element)
 {
 window.setTimeout(callback, 1000 / this.fps);
 };
 // apply to our window global to avoid illegal invocations (it’s a native) return function (callback, element) { func.apply(window, [callback, element]);
 };
 })();
 
基於時間的動畫
 
接下來我們要設置一下貓的奔跑速度。現在,動畫幀根據幀率播放,不同的設備情況有所不同。那就不妙了,因為如果角色移動的同時又有動畫,結果就會看起很來很怪很不協調。你可以試一下控製幀率,但根據真正的定時做出的動畫從各方麵看都更好些。
 
你還會發現,遊戲中的定時通常運用於你所做的一切東西:燃燒率、轉彎速度、加速、跳躍,使用合適的定時,都會有更好的效果。
 
為了讓貓以規定的速度奔跑,我們必須追蹤已經經過的時間,然後根據分配給每幀的時間播放幀。基本步驟是:
 
1、按每秒幾幀設置動畫速度(msPerFrame)。
 
2、當你循環遊戲時,計算一下自最後一幀以後已經經過了多少時間(delta)。
 
3、如果已經經過的時間足夠把動畫幀播完,那麼播放這一幀並設置累積delta為0。
 
4、如果已經經過的時間不夠,那麼記住(累積)delta時間(acDelta)。
 
以下是代碼:

var frame = 0;
 var lastUpdateTime = 0;
 var acDelta = 0;
 var msPerFrame = 100;
 function update() {
 requestAnimFrame(update);
 var delta = Date.now() – lastUpdateTime;
 if (acDelta > msPerFrame)
 {
 acDelta = 0;
 redraw();
 frame++; if
 (frame >= 6) frame = 0;
 } else {
 
acDelta += delta;
 }
 lastUpdateTime = Date.now();
 }
 
載入後,小貓的移動速度會更合理一些。

result 5
 
縮放和旋轉
 
當圖像渲染後,你還是可以使用這個2D畫布來執行各種操作,如旋轉和縮放。
 
例如,把圖像縮小一半。你可以通過添加ctx.scale(0.5, 0.5)來達到效果:

function redraw()
 {
 ctx.fillStyle = ‘#000000′;
 ctx.fillRect(0, 0, canvas.width, canvas.height);
 if (imageReady)
 {
 ctx.save();
 ctx.scale(0.5,0.5);
 ctx.drawImage(img, frame*96, 0, 96, 54,
 canvas.width/2 – 48, canvas.height/2 – 48, 96, 54);
 ctx.restore();
 }
 }
 

result 6
 
你會發現我還在縮放命令前添加了ctx.save(),以及在最後添加了ctx.restore()。沒有這個,縮放命令就會累積,而可憐的小貓就會很快縮小到看不見(試一下,很有意思)。

使用負值還可以達到顛倒圖像的效果。如果你把縮放值從(0.5, 0.5)變成(-1, 1),那麼貓圖像就會水平翻轉,這樣它就會往相反的方向跑。注意,這個轉變是用翻轉起點X位置達到反轉圖像的效果。

function redraw() {
 ctx.fillStyle = ‘#000000′;
 ctx.fillRect(0, 0, canvas.width, canvas.height);
 if (imageReady) { ctx.save();
 ctx.translate(img.width, 0);
 ctx.scale(-1, 1);
 ctx.drawImage(img, frame*96, 0, 96, 54,
 canvas.width/2 – 48, canvas.height/2 – 48, 96, 54);
 ctx.restore();
 }
 }
 
你可以嚐試一下。以下是貓爬牆的動畫(其實是豎直旋轉了動畫):

ctx.rotate( 270*Math.PI/180 );
 ctx.drawImage(img, frame*96, 0, 96, 54,
 -(canvas.width/2 – 48), (canvas.height/2 – 48), 96, 54);
 
在這個例子中,通過旋轉內容,不隻是圖像旋轉了,連坐標也旋轉了,所以drawImage命令通過反轉貓繪製的X位置來抵消這個。

result 7
 
真是一隻天才的貓(不過吸血鬼本來就能爬牆)。
 
縮放和旋轉效果很好。好是好,但它也很慢,會對渲染表現產生重大影響。在製作遊戲時,還有另一個技巧——預渲染,可以解決這個問題以及你可能遇到了大量其他渲染表現問題。
 
預渲染
 
預渲染就是提前處理圖像。你隻做一次昂貴的渲染操作,然後循環使用已渲染好的結果。
 
在HTML5中,你必須在分開的不可見畫布上繪製,然後不是繪製圖像,而是把其他畫布繪製在圖像的位置上。
 
以下是預渲染貓的代碼例子:

var reverseCanvas = null;
 function prerender() {
 reverseCanvas = document.createElement(‘canvas’);
 reverseCanvas.width = img.width;
 reverseCanvas.height = img.height;
 var rctx = reverseCanvas.getContext(“2d”);
 rctx.save(); rctx.translate(img.width, 0);
 rctx.scale(-1, 1);
 rctx.drawImage(img, 0, 0);
 rctx.restore();
 }
 
注意,畫麵對象是創建的,不是添加到文件占的,所以它不會顯示出來。高度和寬度設置到原來的子畫麵表格中,然後原圖像會使用渲染器的2D環境繪製圖像。
 
為了設置預渲染,你可以從loaded功能中調用它。

function loaded() {
 imageReady = true;
 prerender();
 requestAnimFrame(update);
 }
 
然後當你製作定期重繪製命令時,使用reverseCanvas而不是原來的畫布:

function redraw() {
 ctx.fillStyle = ‘#000000′;
 ctx.fillRect(0, 0, canvas.width, canvas.height);
 if (imageReady) {
 ctx.save();
 ctx.drawImage(reverseCanvas, frame*96, 0, 96, 96,
 (canvas.width/2 – 48), (canvas.height/2 – 48), 96, 96);
 ctx.restore();
 }
 }
 
不幸地是,當我們顛倒圖像,動畫也會往後播放,所以你必須把動畫順序也顛倒一下:

function update() {
 requestAnimFrame(update);
 var delta = Date.now() – lastUpdateTime;
 if (acDelta > msPerFrame) {
 acDelta = 0;
 redraw();
 frame–;
 if (frame < 0) frame = 5;
 } else {
 acDelta += delta;
 }
 lastUpdateTime = Date.now();
 }

result 8
 
如果有需要,你可以把畫麵轉換成圖像,即設置它的來源為使用包含編碼圖像數據的數據URL。畫布有方法可以達到這個效果,所以代碼很簡單:

newImage = new Image();
 newImage.src = reverseCanvas.toDataURL(“image/png”);
 
另一個有意思的圖像操作是使用真正的象素數據。HTML5畫布元素把圖像數據當作RGBA格式的象素集合來顯示。代碼如下:
 
var imageData = ctx.getImageData(0, 0, width, height);
 
上述代碼會返回一個包含寬度、高度和數據成員的ImageData結構。這個數據元素就是一個象素的集合。
 
這個數據組是由所有象素點組成的,每個象素點都表現為4個實體,紅、綠、藍和alpha通道層,色彩範圍是0-255。因此一張寬和高都是512的圖像形成的數組就包含1048576個元素,也就是512×512等於262144個象素點再乘上4(每個象素點是4個實體)。
 
使用這個數據組,這裏有一個例子:圖像的特殊紅色成分增加而紅色和藍色成分減少,因此形成我們的2級怪物——地獄惡魔貓。

function prerender() {
 reverseCanvas = document.createElement(‘canvas’);
 reverseCanvas.width = img.width;
 reverseCanvas.height = img.height;
 var rctx = reverseCanvas.getContext(“2d”);
 rctx.save();
 rctx.translate(img.width, 0);
 rctx.scale(-1, 1);
 rctx.drawImage(img, 0, 0);
 // modify the colors var imageData = rctx.getImageData(0, 0, reverseCanvas.width, reverseCanvas.height);
 for (var i=0, il = imageData.data.length;
 i < il; i+=4) {
 if (imageData.data[i] != 0) imageData.data[i] = imageData.data[i] + 100;
 // red
 if (imageData.data[i+1] != 0) imageData.data[i+1] = imageData.data[i+1] – 50;
 // green
 if (imageData.data[i+1] != 0) imageData.data[i+2] = imageData.data[i+2] – 50;
 // blue
 }
 rctx.putImageData(imageData, 0, 0);
 rctx.restore();
 }
 
這個for循環有4次,每一次都修改這3個主色。第4個通道,alpha保持不變,但如果你希望可以使用它變化某些象素的透明度。(注:在下麵的例子中,我們給圖像數據使用一個dataURL,主要是為了避免直接修改象素產生交叉域名問題。你不必在自己的服務器上做這個。)
 
因為使用象素組修改圖象需要重製所有元素,在這個地獄貓的例子中,超過100萬次,你應該盡量提前計算,盡量不要製作變量/對象和跳過象素。

結論

將畫布繪製、縮放、旋轉、轉換和象素修改相結合,再加上預渲染,製作出來的遊戲的動態效果非常棒。
 
我最近在一款2D四方向橫版太空射擊的遊戲《Playcraft》的DEMO中也使用了這些技術。美工隻給每種飛船(玩家和敵人)製作一個幀,之後我再根據我們希望飛船轉向的角度、流暢程度來旋轉和預渲染飛船。我可以在運行時根據飛船類型修改角度——殖家飛船的默認轉向角度是36度(非常流暢),而敵人和對手飛船的是16度(比較卡)。我還添加了一個選項,允許電腦性能比較好的玩家把角度提高到72(最流暢)。另外,飛船的徽章和標誌會根據你所在的隊伍動態地重新著色。這再一次節省了渲染和資源,而且允許飛船顏色根據玩家選擇的隊伍動態地調整。
华体会hth体育网 賞析
  • 2101期學員李思庭作品

    2101期學員李思庭作品

  • 2104期學員林雪茹作品

    2104期學員林雪茹作品

  • 2107期學員趙淩作品

    2107期學員趙淩作品

  • 2107期學員趙燃作品

    2107期學員趙燃作品

  • 2106期學員徐正浩作品

    2106期學員徐正浩作品

  • 2106期學員弓莉作品

    2106期學員弓莉作品

  • 2105期學員白羽新作品

    2105期學員白羽新作品

  • 2107期學員王佳蕊作品

    2107期學員王佳蕊作品

專業問題谘詢

你擔心的問題,火星幫你解答

微信掃碼入群領福利

掃碼領福利最新AI資訊

點擊谘詢
添加老師微信,馬上領取免費課程資源

1. 打開微信掃一掃,掃描左側二維碼

2. 添加老師微信,馬上領取免費課程資源

×

同學您好!

您已成功報名0元試學活動,老師會在第一時間與您取得聯係,請保持電話暢通!
確定