本篇文章給大家?guī)砹死肑avaScript實(shí)現(xiàn)貪吃蛇小游戲的實(shí)例,希望對大家有幫助。
JavaScript實(shí)現(xiàn)貪吃蛇小游戲
功能概述
本程序?qū)崿F(xiàn)了如下功能:
-
貪吃蛇的基本功能
-
統(tǒng)計(jì)得分
-
開始與暫停
-
選擇難度等級
-
設(shè)置快捷鍵
5.1 通過ijkl,wsad也能實(shí)現(xiàn)方向的切換
5.2 通過“P” 表示暫停,“C”表示開始或繼續(xù),"R"表示重新開始
實(shí)現(xiàn)過程
最開始的實(shí)現(xiàn)原理其實(shí)是參考的csdn的一位大神,他用JavaScript20行就實(shí)現(xiàn)了貪吃蛇的基本功能,難等可貴的是還沒有bug,鏈接在此
要實(shí)現(xiàn)貪吃蛇大概有以下幾個(gè)步驟:
-
畫一個(gè)蛇的移動區(qū)域
-
畫一條蛇
-
畫食物
-
讓蛇動起來
-
設(shè)定游戲規(guī)則
-
設(shè)置難度等級
-
設(shè)置開始與暫停
-
設(shè)置游戲結(jié)束后續(xù)操作
-
實(shí)現(xiàn)人機(jī)交互頁面
注:下面的過程講解部分只是講述了部分原理與實(shí)現(xiàn),建議一邊看最后的完整代碼,一邊看下面的講解,更容易理解每一部分的原理與實(shí)現(xiàn)
畫蛇的活動區(qū)域
首先我們畫蛇的活動區(qū)域,我們采用JavaScript的canvas進(jìn)行繪制
我們用一個(gè)400 × 400 400times 400400×400的區(qū)域作為蛇的活動區(qū)域
<canvas id="canvas" width="400" height="400"></canvas>
同時(shí)通過CSS設(shè)置一個(gè)邊界線
#canvas { border: 1px solid #000000; /* 設(shè)置邊框線 */}
畫蛇和食物
效果如下:
在畫蛇前我們需要想下蛇的數(shù)據(jù)結(jié)構(gòu),在這里我們采取最簡單的隊(duì)列表示蛇
-
隊(duì)首表示蛇頭位置,隊(duì)尾表示蛇尾位置
-
我們將之前畫的 400 × 400 400times 400 400×400區(qū)域劃分為400個(gè) 20 × 20 20times 20 20×20的方塊,用這些方塊組成蛇,那么蛇所在方塊的位置的取值范圍就是0~399
舉個(gè)例子:
var snake=[42,41,40];
上述代碼表示蛇所在的位置為42,41,40三個(gè)方塊,其中蛇頭為42,蛇尾為40
對于食物,我們可以用一個(gè)變量food
存儲食物的位置即可,食物的取值范圍為0~399,且不包括蛇的部分,由于游戲中需要隨機(jī)產(chǎn)生食物,隨機(jī)函數(shù)實(shí)現(xiàn)如下:
// 產(chǎn)生min~max的隨機(jī)整數(shù),用于隨機(jī)產(chǎn)生食物的位置function random(min, max) { const num = Math.floor(Math.random() * (max - min)) + min; return num;}
當(dāng)食物被蛇吃掉后就需要重新刷新食物,由于食物不能出現(xiàn)在蛇所在的位置,我們用一個(gè)while循環(huán),當(dāng)食物的位置不在蛇的數(shù)組中則跳出循環(huán)
while (snake.indexOf((food = random(0, 400))) >= 0); // 重新刷新食物,注意食物應(yīng)不在蛇內(nèi)部
我們接下來通過canvas進(jìn)行繪制
首先在js中獲取canvas組件
const canvas = document.getElementById("canvas");const ctx = canvas.getContext("2d");
然后寫繪制函數(shù)用于繪制方格,繪制方格的時(shí)候注意我們預(yù)留1px作為邊框,即我們所畫的方格的邊長為18,我們實(shí)際填充的是18 × 18 18times 1818×18的方塊,方塊的x、y坐標(biāo)(方塊的左上角)的計(jì)算也需要注意加上1px
注:canvas的原點(diǎn)坐標(biāo)在左上角,往右為x軸正方向,往下為y軸正方向
// 用于繪制蛇或者是食物代表的方塊,seat為方塊位置,取值為0~399,color為顏色function draw(seat, color) { ctx.fillStyle = color; // 填充顏色 // fillRect的四個(gè)參數(shù)分別表示要繪制方塊的x坐標(biāo),y坐標(biāo),長,寬,這里為了美觀留了1px用于邊框 ctx.fillRect( (seat % 20) * 20 + 1, Math.floor(seat / 20) * 20 + 1, 18, 18 );}
讓蛇動起來
我們要想讓蛇動起來,首先要規(guī)定蛇運(yùn)動的方向,我們用一個(gè)變量direction
來表示蛇運(yùn)動的方向,其取值范圍為{1,-1,20,-20},1 表示向右,-1 表示向左,20 表示向下,-20 表示向上,運(yùn)動時(shí)只需要將蛇頭的位置加上direction
就可以表示新蛇頭的位置,這樣我們就可以表示蛇的運(yùn)動了。
那么如何讓蛇動起來呢,對于蛇的每次移動,我們需要完成下面幾個(gè)基本操作:
- 將蛇運(yùn)動的下一個(gè)位置變成新蛇頭
- 將下一個(gè)位置加入蛇隊(duì)列
- 繪制下一個(gè)方塊為淺藍(lán)色
- 把舊蛇頭變成蛇身
- 繪制舊蛇頭為淺灰色
- 刪除舊蛇尾
- 將舊蛇尾彈出蛇隊(duì)列
- 繪制舊蛇尾位置為白色
當(dāng)蛇吃掉食物時(shí)(蛇的下一個(gè)位置為食物所在位置)則需更新食物的位置,并繪制新食物為黃色,此時(shí)則不需要?jiǎng)h除舊蛇尾,這樣可以實(shí)現(xiàn)蛇吃完食物后長度的增加功能
我們需要寫一個(gè)函數(shù)實(shí)現(xiàn)上述操作,并且要不斷地調(diào)用這個(gè)函數(shù),從而實(shí)現(xiàn)頁面的刷新,即我們所說的動態(tài)效果
n = snake[0] + direction; // 找到新蛇頭坐標(biāo)snake.unshift(n); // 添加新蛇頭draw(n, "#1a8dcc"); // 繪制新蛇頭為淺藍(lán)色draw(snake[1], "#cececc"); // 將原來的蛇頭(淺藍(lán)色)變成蛇身(淺灰色)if (n == food) { while (snake.indexOf((food = random(0, 400))) >= 0); // 重新刷新食物,注意食物應(yīng)不在蛇內(nèi)部 draw(food, "Yellow"); // 繪制食物} else { draw(snake.pop(), "White"); // 將原來的蛇尾繪制成白色}
接下來,我們需要實(shí)現(xiàn)通過鍵盤控制蛇的運(yùn)動
我們需要獲取鍵盤的key值,然后通過一個(gè)監(jiān)聽函數(shù)去監(jiān)聽鍵盤按下的操作,我們這里通過上下左右鍵(還拓展了WSAD鍵和IJKL鍵控制上下左右方向),同時(shí)設(shè)置一個(gè)變量n
表示下一步的方向
// 用于綁定鍵盤上下左右事件,上下左右方向鍵,代表上下左右方向document.onkeydown = function (event) { const keycode = event.keyCode; if (keycode <= 40) { // 上 38 下 40 左 37 右 39 n = [-1, -20, 1, 20][keycode - 37] || direction; // 若keycode為其他值,即表示按了沒用的鍵,則方向不變 } else if (keycode <= 76 && keycode >= 73) { // i 73 j 74 k 75 l 76 n = [-20, -1, 20, 1][keycode - 73] || direction; } else { switch (keycode) { case 87: //w n = -20; break; case 83: //s n = 20; break; case 65: //a n = -1; break; case 68: //d n = 1; break; default: n = direction; } } direction = snake[1] - snake[0] == n ? direction : n; // 若方向與原方向相反,則方向不變};
設(shè)定游戲規(guī)則
貪吃蛇的最基礎(chǔ)的游戲規(guī)則如下:
- 蛇如果撞到墻或者蛇的身體或尾巴則游戲結(jié)束
- 蛇如果吃掉食物則蛇的長度會增加(上一步已經(jīng)實(shí)現(xiàn))且得分會增加
先實(shí)現(xiàn)第一個(gè),具體如下:
注:下面的一段代碼中的n即為新蛇頭的位置
// 判斷蛇頭是否撞到自己或者是否超出邊界if ( snake.indexOf(n, 1) > 0 || n < 0 || n > 399 || (direction == 1 && n % 20 == 0) || (direction == -1 && n % 20 == 19)) { game_over();}
接下來我們實(shí)現(xiàn)得分統(tǒng)計(jì),對于得分的計(jì)算我們只需要設(shè)置一個(gè)變量score
,用于統(tǒng)計(jì)得分,然后每吃一個(gè)食物,該變量加一,然后將得分信息更新到網(wǎng)頁相應(yīng)位置
score = score + 1;score_cal.innerText = "目前得分: " + score; // 更新得分
設(shè)置難度等級
我們在網(wǎng)頁上設(shè)置一個(gè)單選框,用于設(shè)置難度等級
<form action="" id="mode_form"> 難度等級: <input type="radio" name="mode" id="simply" value="simply" checked /> <label for="simply">簡單</label> <input type="radio" name="mode" id="middle" value="middle" /> <label for="middle">中級</label> <input type="radio" name="mode" id="hard" value="hard" /> <label for="hard">困難</label></form>
效果如下:
那么我們后臺具體如何設(shè)置難度等級的功能呢?
我們采取調(diào)用蛇運(yùn)動的函數(shù)的時(shí)間間隔來代替難度,時(shí)間間隔越小則難度越大,我們分三級:簡單、中級、困難
我們創(chuàng)建一個(gè)時(shí)間間隔變量time_internal
,然后用一個(gè)函數(shù)獲取單選框的取值,并將相應(yīng)模式的時(shí)間間隔賦值給time_internal
// 用刷新間隔代表蛇的速度,刷新間隔越長,則蛇的速度越慢const simply_mode = 200;const middle_mode = 100;const hard_mode = 50;var time_internal = simply_mode; // 刷新時(shí)間間隔,用于調(diào)整蛇的速度,默認(rèn)為簡單模式// 同步難度等級function syncMode() { var mode_value = ""; for (var i = 0; i < mode_item.length; i++) { if (mode_item[i].checked) { mode_value = mode_item[i].value; } } switch (mode_value) { case "simply": time_internal = simply_mode; break; case "middle": time_internal = middle_mode; break; case "hard": time_internal = hard_mode; break; }}
最后只需要在蛇每次移動前調(diào)用一次上述函數(shù)syncMode()
就可以實(shí)現(xiàn)難度切換,至于蛇的速度的具體調(diào)節(jié)且看下面如何講解
設(shè)置開始與暫停
如何實(shí)現(xiàn)蛇的移動動態(tài)效果,如何暫停,如何繼續(xù),速度如何調(diào)節(jié),這就涉及到JavaScript的動畫的部分了,建議看下《JavaScript高級程序設(shè)計(jì)(第4版)》第18章的部分,講的很詳細(xì)。
在最初的“20行JavaScript實(shí)現(xiàn)貪吃蛇”中并沒有實(shí)現(xiàn)開始與暫停,其實(shí)現(xiàn)動態(tài)效果的方法為設(shè)置一個(gè)立即執(zhí)行函數(shù)!function() {}();
,然后在該函數(shù)中使用setTimeout(arguments.callee, 150);
,每隔0.15秒調(diào)用此函數(shù),從而實(shí)現(xiàn)了網(wǎng)頁的不斷刷新,也就是所謂的動態(tài)效果。
后來,我通過web課程老師的案例(彈球游戲)中了解到requestAnimationFrame方法可以實(shí)現(xiàn)動畫效果,于是我便百度查詢,最后在翻書《JavaScript高級程序設(shè)計(jì)(第4版)》第18章動畫與Canvas圖形中得到啟發(fā)–如何實(shí)現(xiàn)開始與取消,如何自定義時(shí)間間隔(實(shí)現(xiàn)難度調(diào)節(jié),蛇的速度)
書中給出的開始動畫與取消動畫的方法如下:
注:為了便于理解,自己修改過原方法
var requestID; // 用于標(biāo)記請求ID與取消動畫 function updateProgress() { // do something... requestID = requestAnimationFrame(updateProgress); // 調(diào)用后在函數(shù)中反復(fù)調(diào)用該函數(shù) } id = requestAnimationFrame(updateProgress); // 第一次調(diào)用(即開始動畫) cancelAnimationFrame(requestID); // 取消動畫
書中講述道:
requestAnimationFrame()已經(jīng)解決了瀏覽器不知道 JavaScript 動畫何時(shí)開始的問題, 以及最佳間隔是多少的問題。······
傳給 requestAnimationFrame()的函數(shù)實(shí)際上可以接收一個(gè)參數(shù),此參數(shù)是一個(gè) DOMHighResTimeStamp 的實(shí)例(比如 performance.now()返回的值),表示下次重繪的時(shí)間。這一點(diǎn)非常重要: requestAnimationFrame()實(shí)際上把重繪任務(wù)安排在了未來一個(gè)已知的時(shí)間點(diǎn)上,而且通過這個(gè)參數(shù) 告訴了開發(fā)者?;谶@個(gè)參數(shù),就可以更好地決定如何調(diào)優(yōu)動畫了。
requestAnimationFrame()返回一個(gè)請求 ID,可以用于通過另一個(gè) 方法 cancelAnimationFrame()來取消重繪任務(wù)
書中同樣給出了如何控制時(shí)間間隔的方法:
書中講述道:
配合使用一個(gè)計(jì)時(shí)器來限制重繪操作執(zhí)行的頻率。這樣,計(jì)時(shí)器可以限制實(shí)際的操作執(zhí)行間隔,而 requestAnimationFrame 控制在瀏覽器的哪個(gè)渲染周期中執(zhí)行。下面的例子可以將回調(diào)限制為不超過 50 毫秒執(zhí)行一次
具體方法如下:
let enabled = true; function expensiveOperation() { console.log('Invoked at', Date.now()); } window.addEventListener('scroll', () => { if (enabled) { enabled = false; requestAnimationFrame(expensiveOperation); setTimeout(() => enabled = true, 50); } });
由上面的方法我得到啟發(fā),在此處我們可以設(shè)置一個(gè)控制函數(shù),用于控制隔一定的時(shí)間調(diào)用一次蛇運(yùn)動的函數(shù),實(shí)現(xiàn)如下:
// 控制游戲的刷新頻率,每隔time_internal時(shí)間間隔刷新一次function game_control(){ if(enabled){ enabled = false; requestAnimationFrame(run_game); setTimeout(() => enabled = true, time_internal); } run_id = requestAnimationFrame(game_control);}// 啟動或繼續(xù)游戲function run_game() { syncMode(); // 同步難度等級 n = snake[0] + direction; // 找到新蛇頭坐標(biāo) snake.unshift(n); // 添加新蛇頭 // 判斷蛇頭是否撞到自己或者是否超出邊界 if ( snake.indexOf(n, 1) > 0 || n < 0 || n > 399 || (direction == 1 && n % 20 == 0) || (direction == -1 && n % 20 == 19) ) { game_over(); } draw(n, "#1a8dcc"); // 繪制新蛇頭為淺藍(lán)色 draw(snake[1], "#cececc"); // 將原來的蛇頭(淺藍(lán)色)變成蛇身(淺灰色) if (n == food) { score = score + 1; score_cal.innerText = "目前得分: " + score; // 更新得分 while (snake.indexOf((food = random(0, 400))) >= 0); // 重新刷新食物,注意食物應(yīng)不在蛇內(nèi)部 draw(food, "Yellow"); // 繪制食物 } else { draw(snake.pop(), "White"); // 將原來的蛇尾繪制成白色 } // setTimeout(arguments.callee, time_internal); //之前的方案,無法實(shí)現(xiàn)暫停和游戲的繼續(xù)}
至于暫停只需要在特定的位置調(diào)用cancelAnimationFrame(run_id);
就可以了
設(shè)置游戲結(jié)束后續(xù)操作
我想的是在游戲結(jié)束后出現(xiàn)一個(gè)“彈窗”,顯示最終得分和是否再來一把
效果如下:
首先,我們實(shí)現(xiàn)網(wǎng)頁的彈窗,通過調(diào)研發(fā)現(xiàn)JavaScript的彈窗可以通過alert()
的方法實(shí)現(xiàn),不過在網(wǎng)頁上直接彈窗感覺不太美觀,而且影響體驗(yàn),于是我想了一下,可以采用一個(gè)p標(biāo)簽實(shí)現(xiàn)偽彈窗,在需要顯示的時(shí)候設(shè)置其display
屬性為block
,不需要顯示的時(shí)候設(shè)置其display
屬性為none
,就類似于Photoshop里面的圖層概念,這樣我們就可以在平常的時(shí)候設(shè)置其display
屬性為none
觸發(fā)game over時(shí)設(shè)置其display
屬性為block
,實(shí)現(xiàn)如下:
<p id="game_over"> <h3 id="game_over_text" align="center">游戲結(jié)束!</h3> <h3 id="game_over_score" align="center">您的最終得分為: 0分</h3> <button id="once_again">再來一把</button> <button id="cancel">取消</button></p>
其CSS部分如下:
#game_over { display: none; /* 設(shè)置game over 窗口不可見 */ position: fixed; top: 190px; left: 65px; width: 280px; height: 160px; background-color: aliceblue; border-radius: 5px; border: 1px solid #000; /* 設(shè)置邊框線 */}#once_again { position: relative; left: 20px;}#cancel { position: relative; left: 50px;}
接下來,我們需要實(shí)現(xiàn)game over的后續(xù)操作:暫停動畫,顯示得分,顯示“彈窗”
function game_over(){ cancelAnimationFrame(run_id); game_over_score.innerText = "您的最終得分為: " + score + "分"; game_over_p.style.display = "block";}
實(shí)現(xiàn)人機(jī)交互頁面
接下來的部分就是提高用戶體驗(yàn)的部分,具體實(shí)現(xiàn)下列功能/操作
- 游戲說明
- 人機(jī)交互按鈕:開始/繼續(xù),暫停,重新開始
- 快捷鍵
- 由于在游戲過程中通過鼠標(biāo)移動到暫停鍵暫停,時(shí)間上太慢,可能造成游戲終止,故應(yīng)該設(shè)置開始/繼續(xù)(C)、暫停(P)、重新開始(R)的快捷鍵
- 有些電腦鍵盤的上下左右鍵較小,操作起來不太方便,可以添加WSAD或者IJKL擴(kuò)展,用于控制上下左右方向
效果如下:
至于寫界面的代碼,可以看文末的完整代碼,這里就稍微講解下綁定按鍵點(diǎn)擊事件與綁定快捷鍵
我們首先看下綁定按鍵點(diǎn)擊事件,點(diǎn)擊”開始/繼續(xù)“只需要調(diào)用requestAnimationFrame(game_control);
,點(diǎn)擊”暫停“只需要調(diào)用cancelAnimationFrame(run_id);
// 綁定開始按鈕點(diǎn)擊事件start_btn.onclick = function () { run_id = requestAnimationFrame(game_control);};// 綁定暫停按鈕點(diǎn)擊事件pause_btn.onclick = function () { cancelAnimationFrame(run_id);};
點(diǎn)擊“重新開始”的話,則需要先暫停動畫,然后刪除畫面上的蛇和食物,初始化所有設(shè)置,然后再調(diào)用requestAnimationFrame(game_control);
開始游戲
注:初始化時(shí)需要初始化得分與難度等級,這里解釋下為什么要將第一個(gè)食物設(shè)置為蛇頭下一個(gè)位置,因?yàn)檫@樣的話蛇會自動先吃一個(gè)食物,繼而可以通過“開始 / 繼續(xù)” 一個(gè)按鈕實(shí)現(xiàn)開始和繼續(xù)操作,同時(shí)run_game()函數(shù)中的食物繪制是在蛇吃到食物之后,保證第一個(gè)食物順利繪制,這樣的話score就需要初始化為-1
// 用于初始化游戲各項(xiàng)參數(shù)function init_game() { snake = [41, 40]; direction = 1; food = 42; score = -1; time_internal = simply_mode; enabled = true; score_cal.innerText = "目前得分: 0分"; // 更新得分 mode_item[0].checked = true; // 重置難度等級為簡單}// 綁定重新開始按鈕點(diǎn)擊事件restart_btn.onclick = function () { cancelAnimationFrame(run_id); // 將原有的食物和蛇的方塊都繪制成白色 for(var i = 0; i < snake.length; i++){ draw(snake[i], "White"); } draw(food, "White"); // 初始化游戲各項(xiàng)參數(shù) init_game(); run_id = requestAnimationFrame(game_control); };
接下來,我們綁定game over中的兩個(gè)按鍵”再來一把“和”取消“
”再來一把“只需要完成“重新開始”里面的事件即可,”取消“只需要完成”重新開始“點(diǎn)擊操作中除了開始游戲的部分,即除了run_id = requestAnimationFrame(game_control);
這兩個(gè)按鈕都需要設(shè)置”彈窗“的display
屬性為none
具體實(shí)現(xiàn)如下:
// 綁定游戲結(jié)束時(shí)的取消按鈕點(diǎn)擊事件cancel_btn.onclick = function () { for(var i = 0; i < snake.length; i++){ draw(snake[i], "White"); } draw(food, "White"); init_game(); game_over_p.style.display = "none";}// 綁定游戲結(jié)束時(shí)的再來一把按鈕點(diǎn)擊事件once_again_btn.onclick = function () { for(var i = 0; i < snake.length; i++){ draw(snake[i], "White"); } draw(food, "White"); init_game(); game_over_p.style.display = "none"; run_id = requestAnimationFrame(game_control);}
最后,我們來講解下如何設(shè)置快捷鍵,快捷鍵只需要用JavaScript模擬點(diǎn)擊對應(yīng)的按鈕即可,實(shí)現(xiàn)如下:
// 同時(shí)綁定R 重啟,P 暫停,C 繼續(xù)document.onkeydown = function (event) { const keycode = event.keyCode; if(keycode == 82){ // R 重啟 restart_btn.onclick(); } else if(keycode == 80){ // P 暫停 pause_btn.onclick(); } else if(keycode == 67){ // C 繼續(xù) start_btn.onclick(); } };
問題、調(diào)試與解決
注: 此部分為本人在實(shí)現(xiàn)過程中出現(xiàn)的bug、調(diào)試過程以及解決方法,感興趣的可以看看,不感興趣的也可以跳過此部分,直接看文末的完整代碼
問題1:點(diǎn)擊暫停和開始,游戲正常開始,按P也可以實(shí)現(xiàn)暫停,按C則畫面出現(xiàn)蛇所在的方格亂跳,無法正常開始,但是按C的操作中只模擬了”開始 / 繼續(xù)“按鈕的點(diǎn)擊?
效果如下:
調(diào)試過程:因?yàn)樯哳^的位置是由direction
控制的,故想到設(shè)置斷點(diǎn),同時(shí)監(jiān)測這個(gè)變量的值的變化,發(fā)現(xiàn)這個(gè)值在按完P(guān)和C時(shí)被更新成很大的數(shù),進(jìn)而去找direction
在哪里被更新,發(fā)現(xiàn)點(diǎn)擊P或C后還需要執(zhí)行下面這一行代碼,而實(shí)際上是不需要的
direction = snake[1] - snake[0] == n ? direction : n; // 若方向與原方向相反,則方向不變
解決方法:只需要執(zhí)行完對應(yīng)的模擬鼠標(biāo)點(diǎn)擊相應(yīng)按鈕事件之后就直接return就可以了
原代碼與修改后的代碼如下:
document.onkeydown = function (event) { const keycode = event.keyCode; if(keycode == 82){ // R 重啟 restart_btn.onclick(); return; // 后來加上的 } else if(keycode == 80){ // P 暫停 pause_btn.onclick(); return; // 后來加上的 } else if(keycode == 67){ // C 繼續(xù) start_btn.onclick(); return; // 后來加上的 } else if (keycode <= 40) { // 上 38 下 40 左 37 右 39 n = [-1, -20, 1, 20][keycode - 37] || direction; // 若keycode為其他值,則方向不變 } else if (keycode <= 76 && keycode >= 73) { // i 73 j 74 k 75 l 76 n = [-20, -1, 20, 1][keycode - 73] || direction; } else { switch (keycode) { case 87: //w n = -20; break; case 83: //s n = 20; break; case 65: //a n = -1; break; case 68: //d n = 1; break; default: n = direction; } } direction = snake[1] - snake[0] == n ? direction : n; // 若方向與原方向相反,則方向不變};
問題2:調(diào)整難度等級后,蛇的速度并沒有發(fā)生改變,但是通過console.log()
發(fā)現(xiàn)確實(shí)調(diào)用了同步難度模式的函數(shù)?
調(diào)試過程:在同步難度等級的函數(shù)中設(shè)置console.log()
方法,輸出time_internal
變量,同時(shí)設(shè)斷點(diǎn)調(diào)試,發(fā)現(xiàn)time_internal
變量不發(fā)生變化,mode_value
變量始終為undefined
,最后發(fā)現(xiàn)應(yīng)該是值傳遞時(shí)的錯(cuò)誤mode_value = mode_item.value;
解決方法:修改值傳遞的方法,加上索引,改為mode_value = mode_item[i].value;
原代碼和修改后的代碼如下:
// 同步難度等級function syncMode() { var mode_value = ""; for (var i = 0; i < mode_item.length; i++) { if (mode_item[i].checked) { mode_value = mode_item[i].value;//原來是mode_item.value } } switch (mode_value) { case "simply": time_internal = simply_mode; break; case "middle": time_internal = middle_mode; break; case "hard": time_internal = hard_mode; break; }}
完整代碼
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>貪吃蛇小游戲</title> <style> button { width: 100px; height: 40px; font-weight: bold; } #game_title { margin-left: 95px; } #canvas { border: 1px solid #000000; /* 設(shè)置邊框線 */ } #score { font-weight: bold; } #mode_form { font-weight: bold; } #game_over { display: none; /* 設(shè)置game over 窗口不可見 */ position: fixed; top: 190px; left: 65px; width: 280px; height: 160px; background-color: aliceblue; border-radius: 5px; border: 1px solid #000; /* 設(shè)置邊框線 */ } #once_again { position: relative; left: 20px; } #cancel { position: relative; left: 50px; } </style> </head> <body> <h1 id="game_title">貪吃蛇小游戲</h1> <canvas id="canvas" width="400" height="400"></canvas> <p id="game_over"> <h3 id="game_over_text" align="center">游戲結(jié)束!</h3> <h3 id="game_over_score" align="center">您的最終得分為: 0分</h3> <button id="once_again">再來一把</button> <button id="cancel">取消</button> </p> <br> <p id="game_info"> <p><b>游戲說明:</b></p> <p> <b>1</b>. 用鍵盤上下左右鍵(或者IJKL鍵,或者WSAD鍵)控制蛇的方向,尋找吃的東西 <br><b>2</b>. 每吃一口就能得到一定的積分,同時(shí)蛇的身子會越吃越長 <br><b>3</b>. 不能碰墻,不能咬到自己的身體,更不能咬自己的尾巴 <br><b>4</b>. 在下方單選框中選擇難度等級,點(diǎn)擊"<b>開始 / 繼續(xù)</b>"即開始游戲,點(diǎn)擊"<b>暫停</b>"則暫停游戲, <br> 再點(diǎn)擊"<b>開始 / 繼續(xù)</b>"繼續(xù)游戲,點(diǎn)擊"重新開始"則重新開始游戲 <br><b>5</b>. <b>快捷鍵</b>: "<b>C</b>"表示開始或繼續(xù),"<b>P</b>"表示暫停,"<b>R</b>"表示重新開始 </p> </p> <p id="score">目前得分: 0分</p> <form action="" id="mode_form"> 難度等級: <input type="radio" name="mode" id="simply" value="simply" checked /> <label for="simply">簡單</label> <input type="radio" name="mode" id="middle" value="middle" /> <label for="middle">中級</label> <input type="radio" name="mode" id="hard" value="hard" /> <label for="hard">困難</label> </form> <br /> <button id="startButton">開始 / 繼續(xù)</button> <button id="pauseButton">暫停</button> <button id="restartButton">重新開始</button> <script> const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const start_btn = document.getElementById("startButton"); const pause_btn = document.getElementById("pauseButton"); const restart_btn = document.getElementById("restartButton"); const once_again_btn = document.getElementById("once_again"); const cancel_btn = document.getElementById("cancel"); const game_over_p = document.getElementById("game_over"); const game_over_score = document.getElementById("game_over_score"); const score_cal = document.getElementById("score"); const mode_item = document.getElementsByName("mode"); // 用刷新間隔代表蛇的速度,刷新間隔越長,則蛇的速度越慢 const simply_mode = 200; const middle_mode = 100; const hard_mode = 50; //注意要改為var const是不會修改的 var snake = [41, 40]; // 蛇身體隊(duì)列 var direction = 1; // 方向:1為向右,-1為向左,20為向下,-20為向上 var food = 42; // 食物位置,取值為0~399 var n; // 蛇的下一步的方向(由鍵盤和蛇的原方向決定) var score = -1; // 得分 var time_internal = simply_mode; // 刷新時(shí)間間隔,用于調(diào)整蛇的速度,默認(rèn)為簡單模式 let enabled = true; // 用于控制是否刷新,實(shí)現(xiàn)通過一定頻率刷新 let run_id; // 請求ID,用于暫停功能 // 產(chǎn)生min~max的隨機(jī)整數(shù),用于隨機(jī)產(chǎn)生食物的位置 function random(min, max) { const num = Math.floor(Math.random() * (max - min)) + min; return num; } // 用于繪制蛇或者是食物代表的方塊,seat為方塊位置,取值為0~399,color為顏色 function draw(seat, color) { ctx.fillStyle = color; // 填充顏色 // fillRect的四個(gè)參數(shù)分別表示要繪制方塊的x坐標(biāo),y坐標(biāo),長,寬,這里為了美觀留了1px用于邊框 ctx.fillRect( (seat % 20) * 20 + 1, Math.floor(seat / 20) * 20 + 1, 18, 18 ); } // 同步難度等級 function syncMode() { var mode_value = ""; for (var i = 0; i < mode_item.length; i++) { if (mode_item[i].checked) { mode_value = mode_item[i].value;//原來是mode_item.value } } switch (mode_value) { case "simply": time_internal = simply_mode; break; case "middle": time_internal = middle_mode; break; case "hard": time_internal = hard_mode; break; } } // 用于綁定鍵盤上下左右事件,我設(shè)置了wsad,或者ijkl,或者上下左右方向鍵,代表上下左右方向 // 同時(shí)綁定R 重啟,P 暫停,C 繼續(xù),注意:若是這幾個(gè)鍵則不需要更新direction的值,操作結(jié)束后直接返回即可 document.onkeydown = function (event) { const keycode = event.keyCode; if(keycode == 82){ // R 重啟 restart_btn.onclick(); return; } else if(keycode == 80){ // P 暫停 pause_btn.onclick(); return; } else if(keycode == 67){ // C 繼續(xù) start_btn.onclick(); return; } else if (keycode <= 40) { // 上 38 下 40 左 37 右 39 n = [-1, -20, 1, 20][keycode - 37] || direction; // 若keycode為其他值,則方向不變 } else if (keycode <= 76 && keycode >= 73) { // i 73 j 74 k 75 l 76 n = [-20, -1, 20, 1][keycode - 73] || direction; } else { switch (keycode) { case 87: //w n = -20; break; case 83: //s n = 20; break; case 65: //a n = -1; break; case 68: //d n = 1; break; default: n = direction; } } direction = snake[1] - snake[0] == n ? direction : n; // 若方向與原方向相反,則方向不變 }; // 用于初始化游戲各項(xiàng)參數(shù) function init_game() { snake = [41, 40]; direction = 1; food = 42; score = -1; time_internal = simply_mode; enabled = true; score_cal.innerText = "目前得分: 0分"; // 更新得分 mode_item[0].checked = true; // 重置難度等級為簡單 } function game_over(){ cancelAnimationFrame(run_id); game_over_score.innerText = "您的最終得分為: " + score + "分"; game_over_p.style.display = "block"; } // 啟動或繼續(xù)游戲 function run_game() { syncMode(); // 同步難度等級 n = snake[0] + direction; // 找到新蛇頭坐標(biāo) snake.unshift(n); // 添加新蛇頭 // 判斷蛇頭是否撞到自己或者是否超出邊界 if ( snake.indexOf(n, 1) > 0 || n < 0 || n > 399 || (direction == 1 && n % 20 == 0) || (direction == -1 && n % 20 == 19) ) { game_over(); } draw(n, "#1a8dcc"); // 繪制新蛇頭為淺藍(lán)色 draw(snake[1], "#cececc"); // 將原來的蛇頭(淺藍(lán)色)變成蛇身(淺灰色) if (n == food) { score = score + 1; score_cal.innerText = "目前得分: " + score; // 更新得分 while (snake.indexOf((food = random(0, 400))) >= 0); // 重新刷新食物,注意食物應(yīng)不在蛇內(nèi)部 draw(food, "Yellow"); // 繪制食物 } else { draw(snake.pop(), "White"); // 將原來的蛇尾繪制成白色 } // setTimeout(arguments.callee, time_internal); //之前的方案,無法實(shí)現(xiàn)暫停和游戲的繼續(xù) } // 控制游戲的刷新頻率,每隔time_internal時(shí)間間隔刷新一次 function game_control(){ if(enabled){ enabled = false; requestAnimationFrame(run_game); setTimeout(() => enabled = true, time_internal); } run_id = requestAnimationFrame(game_control); } // 綁定開始按鈕點(diǎn)擊事件 start_btn.onclick = function () { run_id = requestAnimationFrame(game_control); }; // 綁定暫停按鈕點(diǎn)擊事件 pause_btn.onclick = function () { cancelAnimationFrame(run_id); }; // 綁定重新開始按鈕點(diǎn)擊事件 restart_btn.onclick = function () { cancelAnimationFrame(run_id); // 將原有的食物和蛇的方塊都繪制成白色 for(var i = 0; i < snake.length; i++){ draw(snake[i], "White"); } draw(food, "White"); // 初始化游戲各項(xiàng)參數(shù) init_game(); run_id = requestAnimationFrame(game_control); }; // 綁定游戲結(jié)束時(shí)的取消按鈕點(diǎn)擊事件 cancel_btn.onclick = function () { for(var i = 0; i < snake.length; i++){ draw(snake[i], "White"); } draw(food, "White"); init_game(); game_over_p.style.display = "none"; } // 綁定游戲結(jié)束時(shí)的再來一把按鈕點(diǎn)擊事件 once_again_btn.onclick = function () { for(var i = 0; i < snake.length; i++){ draw(snake[i], "White"); } draw(food, "White"); init_game(); game_over_p.style.display = "none"; run_id = requestAnimationFrame(game_control); } </script> </body></html>
【