貪吃蛇的遊戲相信大家都玩過。在那個水果機還沒有流行,人手一部諾基亞的時代,貪吃蛇是手機中的必備遊戲。筆者閒的無聊的時候就拿出手機來玩上幾局,挑戰一下自己的記錄。
後來上大學了,用c語言做過貪吃蛇的遊戲,不過主要是通過函式來控制(PS:現在讓我看程式碼都看不懂(⊙﹏⊙))。現在學習前端框架之後,通過jQuery來實現一個貪吃蛇的遊戲效果,雖然遊戲介面比(bu)較(ren)簡(zhi)陋(shi),但是主要學習一下游戲中物件導向和由區域性到整體的思想。
設計思想
在開始寫程式碼前首先讓我們來構思一下整體遊戲的實現過程:
需要的物件
首先既然是貪吃蛇,那麼遊戲中肯定要涉及到兩個物件,一個是蛇的物件,另一個是食物的物件。食物物件肯定要有一個屬性就是食物的座標點,蛇物件有一個屬性是一個陣列,用來存放蛇身體所有的座標點。
如何移動
另外全域性需要有一個定時器來週期性的移動蛇的身體。由於蛇的身體彎彎曲曲有各種不同的形狀,因此我們只處理蛇的頭部和尾部,每次移動都根據移動的方向的不同來新增新的頭部,再把尾部擦去,看起來就像蛇在向前爬行一樣。
方向控制
由於蛇有移動的方向,因此我們也需要在全域性定義一個方向物件,物件中有上下左右所代表的值。同時,在蛇物件的屬性中我們也需要定義一個方向屬性,用來表示當前蛇所移動的方向。
碰撞檢測
在蛇向前爬行的過程中,會遇到三種不同的情況,需要進行不同的判斷檢測。第一種情況是吃到了食物,這時候就需要向蛇的陣列中新增食物的座標點;第二種情況是碰到了自己的身體,第三種是碰到了邊界,這兩種情況都導致遊戲結束;如果不是上面的三種情況,蛇就可以正常的移動。
開始程式設計
整體構思有了,下面就開始寫程式碼了。
搭建幕布
首先整個遊戲需要一個搭建活動的場景,我們通過一個表格佈局來作為整個遊戲的背景。
<style type="text/css">
#pannel table{
border-collapse:collapse;
}
#pannel td{
width: 10px;
height: 10px;
border: 1px solid #000;
}
#pannel td.food{
background: green;
}
#pannel td.body{
background: #f60;
}
</style>
<div id="pannel">
</div>
<select name="" id="palSize">
<option value="10">10*10</option>
<option value="20">20*20</option>
<option value="40">30*30</option>
</select>
<select name="" id="palSpeed">
<option value="500">速度-慢</option>
<option value="250">速度-正常</option>
<option value="100">速度-快</option>
</select>
<button id="startBtn">開始</button>複製程式碼
pannel就是我們的幕布,我們在這個裡面用td標籤來畫上一個個的“畫素點”。我們用兩種樣式來表現不同的物件,.body
表示蛇的身體的樣式,.food
表示食物的樣式。
var settings = {
// pannel皮膚的長度
pannelSize: 10,
// 貪吃蛇移動的速度
speed: 500,
// 貪吃蛇工作執行緒
workThread: null,
};
function setPannel(size){
var content = [];
content.push('<table>');
for(let i=0;i<size;i++){
content.push('<tr>');
for(let j=0;j<size;j++){
content.push('<td class="td_'+i+'_'+j+'"></td>');
}
content.push('</tr>');
}
content.push('</table>');
$('#pannel').html(content.join(''));
}
setPannel(settings.pannelSize);複製程式碼
我們定義了一個全域性的settings用來存放全域性性的變數,比如幕布的大小、蛇移動的速度和工作的執行緒。然後通過一個函式把幕布畫了出來,最後的效果就是這樣:
方向和定位
既然我們的“舞臺”已經搭建完了,怎麼來定義我們“演員”的位置和移動的方向呢。首先定義一個全域性的方向變數,對應的數值就是我們的上下左右方向鍵所代表的keyCode。
var Direction = {
UP: 38,
DOWN: 40,
LEFT: 37,
RIGHT: 39,
};複製程式碼
我們在上面畫幕布的時候通過兩次遍歷畫出了一個類似於中學裡學的座標系,有X軸和Y軸。如果每次都用{x:x,y:y}
來表示會很(mei)麻(bi)煩(ge),我們可以定義一個座標點物件。
function Position(x,y){
// 距離X軸長度,取值範圍0~pannelSize-1
this.X = x || 0;
// 距離Y軸長度,取值範圍0~pannelSize-1
this.Y = y || 0;
}複製程式碼
副咖--食物
既然定義好了座標點物件,那麼可以先來看一下簡單的物件,就是我們的食物(Food)物件,上面說了,它有一個重要的屬性就是它的座標點。
function Food(){
this.pos = null;
// 隨機產生Food座標點,避開蛇身
this.Create = function(){
if(this.pos){
this.handleDot(false, this.pos, 'food');
}
let isOk = true;
while(isOk){
let x = parseInt(Math.random()*settings.pannelSize),
y = parseInt(Math.random()*settings.pannelSize);
if(!$('.td_'+x+'_'+y).hasClass('body')){
isOk = false;
let pos = new Position(x, y);
this.handleDot(true, pos, 'food');
this.pos = pos;
}
}
};
// 畫點
this.handleDot = function(flag, dot, className){
if(flag){
$('.td_'+dot.X+'_'+dot.Y).addClass(className);
} else {
$('.td_'+dot.X+'_'+dot.Y).removeClass(className);
}
};
}複製程式碼
既然食物有了座標點這個屬性,那麼我們什麼時候給他賦值呢?我們知道Food是隨機產生的,因此我們定義了一個Create函式用來產生Food的座標點。但是產生的座標點又不能在蛇的身體上,所以通過一個while迴圈來產生座標點,如果座標點正確了,就終止迴圈。此外為了方便我們統一處理座標點的樣式,因此定義了一個handleDot函式。
主咖--蛇
終於到了我們的主咖,蛇。首先定義一下蛇基本的屬性,最重要的肯定是蛇的body屬性,每次移動時,都需要對這個陣列進行一些操作。其次是蛇的方向,我們給它一個預設向下的方向。然後是食物,在蛇的建構函式中我們傳入食物物件,在後續移動時需要判斷是否吃到食物。
function Snake(myFood){
// 蛇的身體
this.body = [];
// 蛇的方向
this.dir = Direction.DOWN;
// 蛇的食物
this.food = myFood;
// 創造蛇身
this.Create = function(){
let isOk = true;
while(isOk){
let x = parseInt(Math.random()*(settings.pannelSize-2))+1,
y = parseInt(Math.random()*(settings.pannelSize-2))+1;
console.log(x,y)
if(!$('.td_'+x+'_'+y).hasClass('food')){
isOk = false;
let pos = new Position(x, y);
this.handleDot(true, pos, 'body')
this.body.push(pos);
}
}
};
this.handleDot = function(flag, dot, className){
if(flag){
$('.td_'+dot.X+'_'+dot.Y).addClass(className);
} else {
$('.td_'+dot.X+'_'+dot.Y).removeClass(className);
}
};
}複製程式碼
移動函式處理
下面對蛇移動的過程進行處理,由於我們每次都採用添頭去尾
的方式移動,因此我們每次只需要關注蛇的頭和尾。我們約定陣列的第一個元素是頭,最後一個元素是尾。
this.Move = function(){
let oldHead = Object.assign(new Position(), this.body[0]),
oldTail = Object.assign(new Position(), this.body[this.body.length - 1]),
newHead = Object.assign(new Position(), oldHead);
switch(this.dir){
case Direction.UP:
newHead.X = newHead.X - 1;
break;
case Direction.DOWN:
newHead.X = newHead.X + 1;
break;
case Direction.LEFT:
newHead.Y = newHead.Y - 1;
break;
case Direction.RIGHT:
newHead.Y = newHead.Y + 1;
break;
default:
break;
}
// 陣列添頭
this.body.unshift(newHead);
// 陣列去尾
this.body.pop();
};複製程式碼
檢測函式處理
這樣我們對蛇身陣列就處理完了。但是我們還需要對新的頭(newHead)進行一些碰撞檢測,判斷新頭部的位置上是否有其他東西(碰撞檢測)。
// 食物檢測
this.eatFood = function(){
let newHead = this.body[0];
if(newHead.X == this.food.pos.X&&newHead.Y == this.food.pos.Y){
return true;
} else {
return false;
}
};
// 邊界檢測
this.konckWall = function(){
let newHead = this.body[0];
if(newHead.X == -1 ||
newHead.Y == -1 ||
newHead.X == settings.pannelSize ||
newHead.Y == settings.pannelSize ){
return true;
} else {
return false;
}
};
// 蛇身檢測
this.konckBody = function(){
let newHead = this.body[0],
flag = false;
this.body.map(function(elem, index){
if(index == 0)
return;
if(elem.X == newHead.X && elem.Y == newHead.Y){
flag = true;
}
});
return flag;
};複製程式碼
重新繪製
因此我們需要對Move函式進行一些擴充:
this.Move = function(){
// ...陣列操作
if(this.eatFood()){
this.body.push(oldTail);
this.food.Create();
this.rePaint(true, newHead, oldTail);
} else if(this.konckWall() || this.konckBody()) {
this.Over();
} else {
this.rePaint(false, newHead, oldTail);
}
};
this.Over = function(){
clearInterval(settings.workThread);
console.log('Game Over');
};
this.rePaint = function(isEatFood, newHead, oldTail){
if(isEatFood){
// 加頭
this.handleDot(true, newHead, 'body');
} else {
// 加頭
this.handleDot(true, newHead, 'body');
// 去尾
this.handleDot(false, oldTail, 'body');
}
};複製程式碼
因為在Move函式處理陣列的後我們的蛇身還沒有重新繪製,因此我們很巧妙地判斷如果是吃到食物的情況,在陣列中就把原來的尾部新增上,這樣就達到了吃食物的效果。同時我們定義一個rePaint函式進行頁面的重繪。
遊戲控制器
我們的“幕布”、“演員”和“動作指導”都已經到位,那麼,我們現在就需要一個“攝影機”進行拍攝,讓它們都開始“幹活”。
function Control(){
this.snake = null;
// 按鈕的事件繫結
this.bindClick = function(){
var that = this;
$(document).on('keydown', function(e){
if(!that.snake)
return;
var canChangrDir = true;
switch(e.keyCode){
case Direction.DOWN:
if(that.snake.dir == Direction.UP){
canChangrDir = false;
}
break;
case Direction.UP:
if(that.snake.dir == Direction.DOWN){
canChangrDir = false;
}
break;
case Direction.LEFT:
if(that.snake.dir == Direction.RIGHT){
canChangrDir = false;
}
break;
case Direction.RIGHT:
if(that.snake.dir == Direction.LEFT){
canChangrDir = false;
}
break;
default:
canChangrDir = false;
break;
}
if(canChangrDir){
that.snake.dir = e.keyCode;
}
});
$('#palSize').on('change',function(){
settings.pannelSize = $(this).val();
setPannel(settings.pannelSize);
});
$('#palSpeed').on('change',function(){
settings.speed = $(this).val();
});
$('#startBtn').on('click',function(){
$('.food').removeClass('food');
$('.body').removeClass('body');
that.startGame();
});
};
// 初始化
this.init = function(){
this.bindClick();
setPannel(settings.pannelSize);
};
// 開始遊戲
this.startGame = function(){
var food = new Food();
food.Create();
var snake = new Snake(food);
snake.Create();
this.snake =snake;
settings.workThread = setInterval(function(){
snake.Move();
},settings.speed);
}
this.init();
}複製程式碼
我們給document繫結一個keydown事件,當觸發按鍵時改變蛇的移動方向,但是如果和當前蛇移動方向相反時就直接return。最後的效果如下:
可以戳這裡檢視實現效果
總結
實現了貪吃蛇的一些基本功能,比如移動、吃點、控制速度等,頁面也比較的簡單,就一個table、select和button。後期可以新增一些其他的功能,比如有計分、關卡等,也可以新增多個點,有的點吃完直接GameOver等等。
本文對你有幫助?歡迎掃碼加入前端學習小組微信群: