想要快速上手HTML5 Canvas小游戲開發?下面通過一個例子來進行手把手教學。(如果你懷疑我的資歷, A Wizard's Lizard這個游戲的半數以上開發是由我完成的)
我們直接來看源碼里的game.js,當然你也可以在線體驗一下游戲先。
游戲截圖
創建畫布
// Create the canvas
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.width = 512;
canvas.height = 480;
document.body.appendChild(canvas);
首先我們需要創建一張畫布作為游戲的舞臺。這里通過JS代碼而不是直接在HTML里寫一個<canvas>
元素目的是要說明代碼創建也是很方便的。有了畫布后就可以獲得它的上下文來進行繪圖了。然后我們還設置了畫布大小,最后將其添加到頁面上。
準備圖片
// 背景圖片
var bgReady = false;
var bgImage = new Image();
bgImage.onload = function () {
bgReady = true;
};
bgImage.src = "images/background.png";
游戲嘛少不了圖片的,所以我們先加載一些圖片先。簡便起見,這里僅創建簡單的圖片對象,而不是專門寫一個類或者Helper來做圖片加載。bgReady
這個變量用來標識圖片是否已經加載完成從而可以放心地使用了,因為如果在圖片加載未完成情況下進行繪制是會報錯的。
整個游戲中需要用到的三張圖片:背景,英雄及怪物我們都用上面的方法來處理。
游戲對象
// 游戲對象
var hero = {
speed: 256, // 每秒移動的像素
x: 0,
y: 0
};
var monster = {
x: 0,
y: 0
};
var monstersCaught = 0;
現在定義一些對象將在后面用到。我們的英雄
有一個speed
屬性用來控制他每秒移動多少像素。怪物
游戲過程中不會移動,所以只有坐標屬性就夠了。monstersCaught
則用來存儲怪物被捉住的次數。
處理用戶的輸入
// 處理按鍵
var keysDown = {};
addEventListener("keydown", function (e) {
keysDown[e.keyCode] = true;
}, false);
addEventListener("keyup", function (e) {
delete keysDown[e.keyCode];
}, false);
現在開始處理用戶的輸入(對初次接觸游戲開發的前端同學來說,這部分開始可能就需要一些腦力了)。在前端開發中,一般是用戶觸發了點擊事件然后才去執行動畫或發起異步請求之類的,但這里我們希望游戲的邏輯能夠更加緊湊同時又要及時響應輸入。所以我們就把用戶的輸入先保存下來而不是立即響應。
為此,我們用keysDown
這個對象來保存用戶按下的鍵值(keyCode
),如果按下的鍵值在這個對象里,那么我們就做相應處理。
開始一輪游戲
// 當用戶抓住一只怪物后開始新一輪游戲
var reset = function () {
hero.x = canvas.width / 2;
hero.y = canvas.height / 2;
// 將新的怪物隨機放置到界面上
monster.x = 32 + (Math.random() * (canvas.width - 64));
monster.y = 32 + (Math.random() * (canvas.height - 64));
};
reset
方法用于開始新一輪和游戲,在這個方法里我們將英雄放回畫布中心同時將怪物放到一個隨機的地方。
更新對象
// 更新游戲對象的屬性
var update = function (modifier) {
if (38 in keysDown) { // 用戶按的是↑
hero.y -= hero.speed * modifier;
}
if (40 in keysDown) { // 用戶按的是↓
hero.y += hero.speed * modifier;
}
if (37 in keysDown) { // 用戶按的是←
hero.x -= hero.speed * modifier;
}
if (39 in keysDown) { // 用戶按的是→
hero.x += hero.speed * modifier;
}
// 英雄與怪物碰到了么?
if (
hero.x <= (monster.x + 32)
&& monster.x <= (hero.x + 32)
&& hero.y <= (monster.y + 32)
&& monster.y <= (hero.y + 32)
) {
++monstersCaught;
reset();
}
};
這就是游戲中用于更新畫面的update
函數,會被規律地重復調用。首先它負責檢查用戶當前按住的是中方向鍵,然后將英雄往相應方向移動。
有點費腦力的或許是這個傳入的modifier
變量。你可以在main
方法里看到它的來源,但這里還是有必要詳細解釋一下。它是基于1開始且隨時間變化的一個因子。例如1秒過去了,它的值就是1,英雄的速度將會乘以1,也就是每秒移動256像素;如果半秒鐘則它的值為0.5,英雄的速度就乘以0.5也就是說這半秒內英雄以正常速度一半的速度移動。理論上說因為這個update
方法被調用的非常快且頻繁,所以modifier
的值會很小,但有了這一因子后,不管我們的代碼跑得快慢,都能夠保證英雄的移動速度是恒定的。
現在英雄的移動已經是基于用戶的輸入了,接下來該檢查移動過程中所觸發的事件了,也就是英雄與怪物相遇。這就是本游戲的勝利點,monstersCaught
+1然后重新開始新一輪。
渲染物體
// 畫出所有物體
var render = function () {
if (bgReady) {
ctx.drawImage(bgImage, 0, 0);
}
if (heroReady) {
ctx.drawImage(heroImage, hero.x, hero.y);
}
if (monsterReady) {
ctx.drawImage(monsterImage, monster.x, monster.y);
}
// 計分
ctx.fillStyle = "rgb(250, 250, 250)";
ctx.font = "24px Helvetica";
ctx.textAlign = "left";
ctx.textBaseline = "top";
ctx.fillText("Monsterrs caught: " + monstersCaught, 32, 32);
};
之前的工作都是枯燥的,直到你把所有東西畫出來之后。首先當然是把背景圖畫出來。然后如法炮制將英雄和怪物也畫出來。這個過程中的順序是有講究的,因為后畫的物體會覆蓋之前的物體。
這之后我們改變了一下Canvas
的繪圖上下文的樣式并調用fillText
來繪制文字,也就是記分板那一部分。本游戲沒有其他復雜的動畫效果和打斗場面,繪制部分大功告成!
主循環函數
// 游戲主函數
var main = function () {
var now = Date.now();
var delta = now - then;
update(delta / 1000);
render();
then = now;
// 立即調用主函數
requestAnimationFrame(main);
};
上面的主函數控制了整個游戲的流程。先是拿到當前的時間用來計算時間差(距離上次主函數被調用時過了多少毫秒)。得到modifier
后除以1000(也就是1秒中的毫秒數)再傳入update
函數。最后調用render
函數并且將本次的時間保存下來。
關于游戲中循環更新畫面的討論可參見「Onslaught! Arena Case Study」。
關于循環的進一步解釋
// requestAnimationFrame 的瀏覽器兼容性處理
var w = window;
requestAnimationFrame = w.requestAnimationFrame || w.webkitRequestAnimationFrame || w.msRequestAnimationFrame || w.mozRequestAnimationFrame;
如果你不是完全理解上面的代碼也沒關系,我只是覺得拿出來解釋一下總是極好的
為了循環地調用main
函數,本游戲之前用的是setInterval
。但現今已經有了更好的方法那就是requestAnimationFrame
。使用新方法就不得不考慮瀏覽器兼容性。上面的墊片就是出于這樣的考慮,它是Paul Irish 博客原版的一個簡化版本。
啟動游戲!
// 少年,開始游戲吧!
var then = Date.now();
reset();
main();
總算完成了,這是本游戲最后一段代碼了。先是設置一個初始的時間變量then
用于首先運行main
函數使用。然后調用 reset
函數來開始新一輪游戲(如果你還記得的話,這個函數的作用是將英雄放到畫面中間同時將怪物放到隨機的地方以方便英雄去捉它)。
文章列表
留言列表