構建Block構造方法
我們將定義一個Block 構造方法,它會創建對象來表示不可見的游戲網格中的單個的塊。每個塊都有 col(column 的縮寫)和 row 屬性, 它們將存儲特定的塊在網格上的位置。下圖展示了這個帶有數目固定的一 些列和行的網格。盡管這個網格并不會真的出現在屏幕上,游戲設計成讓蘋果和貪吃蛇段總是能夠和網格中的塊對齊。
我們可以通過該構造方法創建一個塊實例,比如下面創建了一個第5列第5行中的塊
var sampleBlock = new Block(5,5);
上面只是創建了實例,但是沒有將實例繪制出來,要在游戲面板上繪制出該實例還必須使用drawSquare或drawCircle方法。
添加equal方法
在游戲中,需要知道兩個塊是否位于同一位置。例如,如果蘋果和貪吃蛇的頭部位于同一位置,這意味著,貪吃蛇會吃掉蘋果。另一方面,如果貪吃蛇的頭部和尾部位于同一位置,那么,貪吃蛇碰到了自己。
為了使得比較塊的位置更為容易,我們給Block 構造方法原型添加了 equal 方法。當在一個塊對象上調用equal并傳遞另一個對象作為一個參數, 如果兩個對象位于相同的位置,它將返回true(否則的話,返回false)。代碼如下:
Block.prototype.equal = function (otherBlock){
return this.col === otherBlock.col && this.row === otherBlock.row;
}
我們將使用 equal 方法來檢查貪吃蛇是否吃到蘋果或者碰到自己。
創建貪吃蛇
我們把貪吃蛇的位置存儲為一個名為segments 的數組,其中包含了一系列的塊對象。為了移動貪吃蛇,我們在segments 數組的 開頭添加一個新的塊,并且從數組的尾部刪除該塊。Segments 數組的第一個元素將表示貪吃蛇的頭部。
- Snake構造方法,貪吃蛇一開始長度為3個網格
direction 屬性存儲了貪吃蛇的當前位置
nextDirection 屬性,它存儲了貪吃蛇在下一個動畫步驟將要移動的方向,除非按下方向鍵修改該屬性,不然貪吃蛇的前進方向不會改變。構造方法將這兩個屬性都設置為 "right",因此游戲一開始的時候,貪吃蛇向右移動。
var Snake = function (){
this.segments = [
new Block(7,5),
new Block(6,5),
new Block(5,5)
];
this.direction = "right";
this.nextDirection = "right";
};
繪制貪吃蛇
為了繪制貪吃蛇,我們直接遍歷其segments 數組中的每一個塊,在每個 塊上調用在前面所創建的drawSquare 方法。這將會為貪吃蛇的每一段都繪制 一個方塊。
Snake.prototype.draw = function(){
for(var i=0;i < this.segments.length; i++){
this.segments[i].drawSquare("Blue"); //等價于Block實例.drawSquare("Blue");
}
};
移動貪吃蛇
貪吃蛇移動看起來好像很復雜,身體的每個塊都得移動,有的塊可能方向可能會變。其實很簡單,我們不用去移動貪吃蛇每個網格組成部分,只要往頭部添加一個網格,把最尾部網格刪除,其他網格不動就可以了。
我們將創建一個 move 方法,沿著貪吃蛇的當前方向將其移動一個塊。為 了移動貪吃蛇,我們添加了一個新的頭部段(在segments 數組的開頭添加了一個新的block 對象),然后,從segments 數組 刪除尾部段。
move方法還將調用一個checkCollision方法, 來查看新的頭部是否與貪吃蛇其他的部分或者墻 發生碰撞,以及新的頭部是否吃到了蘋果。如果新的頭部與身體或墻發生碰撞,調用gameOver 函數來結束游戲。如果貪吃蛇吃到了蘋果,我們增加分數,并且將蘋果移動到 新的位置。
將 this.direction 設置為和this. nextDirection 相等,這會將貪吃蛇的移動方向更 新為與近按下的箭頭鍵一致(當我們介紹keydown 事件處理程序的時候, 將會更詳細地看到這是如何工作的)。
對于動畫中的每一步,貪吃蛇的 direction 屬性都會更新一次,因為每個動畫步驟都會調用一次move 方法。另一方面,當玩家在任何時候按下 一個箭頭鍵,nextDirection屬性都會更新(因此,如果玩家按鍵真的很快, 這個屬性理論上可能會在每個動畫步驟中更新多次)。通過保持這兩個 屬性各自分離,我們確保了如果玩家在動畫的兩個步驟之間非常快速地 按下兩個箭頭鍵,貪吃蛇不會回過頭來碰到自己。
檢查碰撞并添加頭部
只有在checkCollision 返回true 的時候,才會遇到return關鍵字,因此,如果貪吃蛇沒有和任何物體發生碰撞,將會執行剩余的代碼。只要貪吃蛇沒有和某個物體碰撞,就會在貪吃蛇的前面添加新的頭部, 通過使用unshift 把 newHead 添加到segments 數組的開始,從而做到這一點。
貪吃蛇移動只要往頭部添加一個網格,把最尾部網格刪除,其他網格不動就可以了。由于貪吃蛇每個網格是保存在segments屬性數組里面的,于是我們只要每次移動往該數組第一個網格添加一個頭部元素(unshift添加一個元素),如果是吃到蘋果那么就不用改變尾部,如果不是就通過pop把最后一個網格元素去掉。下次根據新的數組繪制出新的貪吃蛇。
數組unshift的用法:用unshift 把元素"Monkey" 和 "Polar Bear" 添加到數組的起始位 置,每一次原有的值都會向后順延 一個索引位置。每次調用unshift,也會返回數 組新的長度,就像 push 一樣。
使用equal 方法來比較newHead 和 apple.position。如果這兩 個塊位于相同的位置,equal 方法將會返回true,這意味著,貪吃蛇吃掉了蘋果。
//創建一個新的網格加到當前貪吃蛇前進方向的頭部,刪除尾部網格
Snake.prototype.move = function(){
var head = this.segments[0];
var newHead;
this.direction = this.nextDirection;
if(this.direction === "right"){
newHead = new Block(head.col+1,head.row);
}else if(this.direction === "down"){
newHead = new Block(head.col,head.row+1);
}else if(this.direction === "left"){
newHead = new Block(head.col-1,head.row);
}else if(this.direction === "up"){
newHead = new Block(head.col,head.row-1);
}
if (this.checkCollision(newHead)){
gameOver();
return;
}
this.segments.unshift(newHead);
if(newHead.equal(apple.position)){
score++;
apple.move();
}else{
this.segments.pop();
}
}
添加 checkCollision 方法
每次為貪吃蛇的頭部設置一個新的位置的時候,都必須檢查碰撞。碰撞檢測在游戲機制中是一個很常見的步驟,往往也是游戲編程中較為復雜 的一個方面。好在,在貪吃蛇游戲中,碰撞檢測相對簡單。
注意:該游戲中(0,0)這樣的位置不是活動位置邊界,已經是由灰色邊框網格填充了。游戲中四個邊界都有10像素寬的邊界,所以貪吃蛇頭部網格的新位置head.col === 0就“Game Over”了。
添加 keydown 事件處理程序
var directions = {
37: "left",
38: "up",
39: "right",
40: "down"
};
$("body").keydown(function(event){
var newDirection = directions[event.keyCode];
if(newDirection !== undefined){
snake.setDirection(newDirection);
}
});
游戲入口程序
var intervalId = setInterval(function(){
ctx.clearRect(0,0,width,height);
drawScore();
snake.move();
snake.draw();
apple.draw();
drawBorder();
},100);
移動蘋果
由于邊框的存在,蘋果出現的范文只能(1~38)*10,0和39都是邊框。
調用 Math.? oor(Math.random() * 38),它給出了從0到37的一個隨機數,然后,給結果加1以得到1到38之間的一個數字。
添加 setDirection 方法
setDirection 還防止玩家調頭以導致貪吃蛇立即碰到自己。例如,如果貪吃蛇向右移動,然后它突然向左轉而不向上或向下移 以改變路徑,那么它會和自己碰撞。這種現象叫作非法調頭,因為我們不想讓玩家這么做。也就是如果當前方向是向右,那么按鍵是向左直接return,不更新當前方向。
完整可運行代碼
<!DOCTYPE html>
<html>
<head>
<title>Learn</title>
</head>
<body>
<canvas id="canvas" width="400" height="400"></canvas>
<script type="text/javascript" src="jquery-3.1.1.js"></script>
<script type="text/javascript">
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var width = canvas.width;
var height = canvas.height;
//游戲面板看成是40個10*10的網格組成的
var blockSize = 10;//每個網格的大小
var widthInBlocks = width / blockSize;
var heightInBlocks = height / blockSize;
//每次貪吃蛇吃掉一個蘋果的時候,分數加1
var score = 0;
//創建一個drawBorder函數來繪制圍繞畫布的灰色邊框,1個塊(10 像素)那么寬。
var drawBorder = function(){
ctx.fillStyle = "Gray";
//繪制4條邊框
ctx.fillRect(0,0,width,blockSize);
ctx.fillRect(0,height-blockSize,width,blockSize);
ctx.fillRect(0,0,blockSize,height);
ctx.fillRect(width-blockSize,0,blockSize,height);
}
//編寫記分函數
var drawScore = function(){
ctx.font = "20px Courier";
ctx.fillStyle = "Black";
ctx.textAlign = "left";
ctx.textBaseline = "top"; //文本左對齊
ctx.fillText("Score:"+score,blockSize,blockSize);
}
//結束游戲
var gameOver = function(){
clearInterval(intervalId);
ctx.font = "60px Courier";
ctx.fillStyle = "Black";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Game Over",width/2,height/2);
}
//繪制圓的方法
var circle = function(x,y,radius,fillCircle){
ctx.beginPath();
ctx.arc(x,y,radius,0,Math.PI*2,false);
if(fillCircle){
ctx.fill();
}else{
ctx.stroke();
}
};
//Block構造方法
var Block = function(col,row){
this.col = col;
this.row = row;
};
//繪制方塊
Block.prototype.drawSquare = function(color){
var x = this.col * blockSize;
var y = this.row * blockSize;
ctx.fillStyle = color;
ctx.fillRect(x,y,blockSize,blockSize);
}
//繪制圓封裝成Block的原型方法
Block.prototype.drawCircle = function(color){
var centerX = this.col * blockSize + blockSize/2;
var centerY = this.row * blockSize + blockSize/2;
ctx.fillStyle = color;
circle(centerX,centerY,blockSize/2,true);
}
//檢查Block是否再同一個位置已經是其他的Block了(如貪吃蛇前進,后面的塊頂替前面塊的位置,都是在同一個位置卻是不同的塊了)
Block.prototype.equal = function (otherBlock){
return this.col === otherBlock.col && this.row === otherBlock.row;
}
//貪吃蛇構造方法
var Snake = function (){
this.segments = [
new Block(7,5),
new Block(6,5),
new Block(5,5)
];
this.direction = "right";
this.nextDirection = "right";
};
//繪制貪吃蛇身體的每一個網格部分
Snake.prototype.draw = function(){
for(var i=0;i < this.segments.length; i++){
this.segments[i].drawSquare("Blue");
}
};
//創建一個新的網格加到當前貪吃蛇前進方向的頭部
Snake.prototype.move = function(){
var head = this.segments[0];
var newHead;
this.direction = this.nextDirection;
if(this.direction === "right"){
newHead = new Block(head.col+1,head.row);
}else if(this.direction === "down"){
newHead = new Block(head.col,head.row+1);
}else if(this.direction === "left"){
newHead = new Block(head.col-1,head.row);
}else if(this.direction === "up"){
newHead = new Block(head.col,head.row-1);
}
if (this.checkCollision(newHead)){
gameOver();
return;
}
this.segments.unshift(newHead);
if(newHead.equal(apple.position)){
score++;
apple.move();
}else{
this.segments.pop();
}
}
//檢查蛇的頭部是否碰到墻體或者吃到自己的尾巴
Snake.prototype.checkCollision = function(head){
var leftCollision = (head.col === 0);
var topCollision = (head.row === 0);
var rightCollision = (head.col === widthInBlocks-1);
var bottomCollision = (head.row === heightInBlocks-1);
var wallCollision = leftCollision || topCollision ||
rightCollision || bottomCollision;
var selfCollision = false;
for(var i=0;i<this.segments.length;i++){
if(head.equal(this.segments[i])){
selfCollision = true;
}
}
return wallCollision || selfCollision;
}
//根據按下的鍵盤決定貪吃蛇下次的移動方向
Snake.prototype.setDirection = function(newDirection){
if(this.direction === "up" && newDirection === "down" ){
return;
}else if(this.direction === "right" && newDirection === "left"){
return;
}else if(this.direction === "down" && newDirection === "up"){
return;
}else if(this.direction === "left" && newDirection === "right"){
return;
}
this.nextDirection = newDirection;
}
//蘋果構造器
var Apple = function(){
this.position = new Block(10,10);
};
//添加原型方法,在蘋果的坐標屬性的位置上繪制一個蘋果
Apple.prototype.draw = function(){
this.position.drawCircle("LimeGreen");
};
//移動蘋果到一個新的隨機位置
Apple.prototype.move = function(){
var randomCol = Math.floor(Math.random()*(widthInBlocks-2))+1;
var randomRow = Math.floor(Math.random()*(heightInBlocks-2))+1;
this.position = new Block(randomCol,randomRow);
};
//游戲開始時,創建貪吃蛇和蘋果
var snake = new Snake();
var apple = new Apple();
//游戲動畫,加載script便簽就得到間歇調用的執行。其他代碼比如函數定義也是執行了,只不過都是函數定義
var intervalId = setInterval(function(){
ctx.clearRect(0,0,width,height);
drawScore();
snake.move();
snake.draw();
apple.draw();
drawBorder();
},100);
//keyCode用更形象的字符串來表示方向
var directions = {
37: "left",
38: "up",
39: "right",
40: "down"
};
$("body").keydown(function(event){
var newDirection = directions[event.keyCode];
if(newDirection !== undefined){
snake.setDirection(newDirection);
}
});
</script>
</body>
</html>