HTML5 Canvas 射擊類小遊戲 平滑的移動 思路
這篇部落格主要講了如何處理HTML5 Canvas 遊戲中的角色移動問題。
筆者這幾天做了一個 HTML5 Canvas 的射擊類小遊戲,嗯,名字叫做《DroppingBalls》,大概就是自己控制一個坦克在介面的最下面左右移動,然後上面會有怪向下移動,我們必須趕在怪完全掉下來之前將它擊斃,否則我們會掉血。怪會越來越多越來越強,我們最後一定會死掉的,這沒關係,因為我們的目前是在死之前拿更多的分數。
這個遊戲目前還沒有完全做完,也僅僅是練習作。遊戲掛在筆者的個人網站上了,大家可以去看一下。
該部落格中的程式碼僅為了表明思路,並不保證可以直接執行。若需要可以直接執行的程式碼,可以到該遊戲頁面 f12 檢視:
好了,今天的話題就是 如何實現這樣的射擊類小遊戲的第一步,完美的控制己方坦克移動。
##主要技術
JS + Canvas
程式碼難度低,只要自己寫過一遍就感覺非常非常簡單了。程式碼僅是工具,但將許多程式碼組合在一起並讓它們高效的工作,這就是一門藝術了。
##遊戲分析
射擊類遊戲,比如坦克大戰、打飛機等等,基本元素幾乎相同。我們從最簡單的入手來看一下。我們就以坦克舉例。
首先得有玩家的坦克,然後玩家會通過鍵盤來控制自己的坦克移動,通過鍵盤控制射擊,射擊後會發出子彈,子彈在射出後會自動飛行,直到擊中敵方坦克或者飛出地圖後消失。敵方坦克的移動路線可以是隨機的,也可以是設定好的。在《DroppingBalls》中,己方坦克、敵方坦克、子彈 全都是一個球Ball, 這樣做的目的是因為相比於正方形等,兩個圓形之間的距離是最好判斷的,這將使【子彈是否能夠擊毀坦克】的判斷非常容易。
遊戲中,玩家通過wasd來控制移動,j來射擊。
HTML頁面中的Canvas標籤id=CanvasGame, 寬=600,高=600
##分步驟實現 第1步
我們要畫一個球(這裡的球和圓是一個意思),表示己方坦克。
這個可以在w3school上自己看一下,這個可以做到之後就可以進行第二步了。
w3school的Canvas教程連結:
##分步驟實現 第2步
實現己方坦克的粗糙的移動。這裡我們只說向右移動,其他方向的類似。
經過第1步的學習,球可以畫出來了,但是怎樣實現球的移動呢?這裡就涉及到動畫的原理了,即球是不會動的,球的移動只是看起來在移動而已,實際上,我們要做的是不斷的清空遊戲畫面,之後,在球的原本位置靠右一點點的位置畫一個球,當我們每秒重複幾十次這樣的清空重畫後,在視覺上,球就動起來了。
總結一下我們要做的事情: 捕獲鍵盤按鍵‘D’時,球的x座標增大,清空遊戲畫面,重新畫出這個球。
這時為了方便各種變數的操作,我們本著面向物件的原則將球封裝一下:
// Base class for myTank 、 enemy 、bullet .etc
function Ball(paraX,paraY,paraR,paraSpeed,paraColor,paraMap) {
var ball = {
x:paraX,
y: paraY,
r:paraR,
speed:paraSpeed,
color: paraColor,
map: paraMap
}
return ball;
}
這裡的speed、map可以先不用管。
通過這樣的封裝,我們很容易實現球的移動:
// Base class for myTank 、 enemy 、bullet .etc
function Ball(paraX,paraY,paraR,paraSpeed,paraColor,paraMap) {
var ball = {
x:paraX,
y: paraY,
r:paraR,
speed:paraSpeed,
color: paraColor,
map: paraMap,
}
ball.move=function(){
this.x+=this.speed;
};
return ball;
}
通過在ball這個物件裡面增加move方法,我們想向右移動該球時,只需要呼叫ball.move()就可以了。
接下來我們要做的就是捕獲鍵盤按鍵‘D’時,呼叫ball.move()
var myTank=new Ball(200,200,20,3,"red",map);//map先不用管
document.onkeydown = function (event) {
var e = event || window.event || arguments.callee.caller.arguments[0];
if (!e) return;
console.log(String.fromCharCode(e.keyCode) + " down");
switch (e.keyCode) {
case "d".charCodeAt(0):
case "D".charCodeAt(0):
myTank.move();
break;
} // end switch
};// end onkeydown
這樣的話就實現了myTank的向右移動。但此時的移動僅僅是在記憶體完成的,我們需要將它畫到Canvas上才能看到。
function clear() {
var ctx =document.getElementById('CanvasGame').getContext('2d');
ctx.clearRect(0, 0, 600, 600);
}
function paintMyTank(){
var ctx = document.getElementById('CanvasGame').getContext('2d');
ctx.beginPath();
ctx.fillStyle = myTank.color;
ctx.arc(myTank.x,myTank.y, myTank.r, 0, 2*Math.PI);
ctx.fill();
}
有了這兩個方法後,我們就可以在捕獲鍵盤按鍵‘D’後呼叫了。區域性程式碼如下:
switch (e.keyCode) {
case "d".charCodeAt(0):
case "D".charCodeAt(0):
myTank.move();
clear();
paintMyTank();
break;
} // end switch
這樣基本實現了myTank的向右移動了。
補全一下ball.move()及document.onkeydown()後,即可實現4個方向的移動。
##分步驟實現 第3步
整理程式碼,實現myTank的完美移動。
經過第二步可以實現非常粗糙的移動,即捕獲方向按鍵後,更新myTank的座標值,清空Canvas,重新畫出myTank。 但很遺憾,真正的程式碼並不是這樣寫的。上面的程式碼僅僅是為了便於理解。第2步實現後,移動非常粗糙,且不能斜方向移動。而且程式碼難以整合。
所在這裡我們要整理一下程式碼,同時解決這個移動的問題。
程式碼的結構上,我們希望每個變數、每個方法都有自己的域,讓程式碼最大可能的受控,說白了,就是面向物件的方法,一層又一層的封裝。這裡我們將遊戲的所有變數及方法封裝到game物件裡面。
function Game() {
var game={};
game.controlKeys=new ControlKeys(); // 控制按鈕, 構造方法在下面
game.models = {
myTank: new MyTank(), //類似的方法實現myTank的構造
};
game.func = {}; //後面會補充
game.event={}; //後面會補充
}
myTank的移動處理上,我們不希望每個鍵盤事件時重新整理一次畫面。而且js無法處理兩個方向鍵同時按下的時候的斜方向移動,所以我們要換一種方式。我們設定一些標誌變數,並將他們封裝到controlKeys物件中:
//game.controlKeys
function ControlKeys() {
var controlKeys = {
//方向
w: false,
a: false,
s: false,
d: false,
}
return controlKeys;
}
鍵盤事件需要捕獲onkeydown和onkeyup, down時,controlKeys對應的標誌變為true;up時變為false;
遊戲時鐘設定為30毫秒, 即每30毫秒,我們根據controlKeys的標誌值,若w為true則myTank.move(“w”),即向上移動;若d為true則myTank.move(“d”),即向右移動。這些判斷之間相互不干涉,這樣就實現了斜方向的移動。
game.event = {
beginControl: function () {
document.onkeydown = function (event) {
var e = event || window.event || arguments.callee.caller.arguments[0];
if (!e) return;
console.log(String.fromCharCode(e.keyCode) + " down");
switch (e.keyCode) {
case "w".charCodeAt(0):
case "W".charCodeAt(0):
game.controlKeys.w = true;
break;
//a s d 類似
} // end switch
};// end onkeydown
document.onkeyup = function (event) {
var e = event || window.event || arguments.callee.caller.arguments[0];
if (!e) return;
console.log(String.fromCharCode(e.keyCode) + " up");
switch (e.keyCode) {
case "w".charCodeAt(0):
case "W".charCodeAt(0):
game.controlKeys.w = false;
break;
//a s d 類似
} // end switch
};//end onkeyup
}
}
game.func = {
//遊戲時鐘; 遊戲週期;
clock: function () {
//處理myTank移動
if (game.controlKeys.w) {
game.models.myTank.move("w");
}
if (game.controlKeys.a) {
game.models.myTank.move("a");
}
//s d 類似
game.painter.paintGame();
}//end game.func.clock()
}; //end game.func
game.event = {
//開啟遊戲時鐘,設定為30毫秒執行一次
start: function () {
setInterval(game.func.clock, 30);
game.func.beginControl();
},
}
這樣,即可實現myTank的平滑的、8個方向的完美移動了。
##結語
《DroppingBalls》 遊戲程式碼目前500行左右,雖然不大,但若想詳細的在一篇部落格講完還是有些困難的。 這篇部落格將myTank的完美移動的思路講了一下,部落格中所給程式碼上可能存在一些bug,因為這些程式碼是從遊戲原始碼中抽出來的部分,僅僅為了表達思路,直接copy過去是不能執行的。
遊戲連結會給到大家,大家可以直接f12看一下原始碼,這樣更方便快捷: