溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何使用JavaScript實(shí)現(xiàn)貪吃蛇小游戲

發(fā)布時(shí)間:2022-01-05 17:35:36 來(lái)源:億速云 閱讀:138 作者:小新 欄目:web開(kāi)發(fā)

這篇文章將為大家詳細(xì)講解有關(guān)如何使用JavaScript實(shí)現(xiàn)貪吃蛇小游戲,小編覺(jué)得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。

JavaScript實(shí)現(xiàn)貪吃蛇小游戲

功能概述

本程序?qū)崿F(xiàn)了如下功能:

  1. 貪吃蛇的基本功能

  2. 統(tǒng)計(jì)得分

  3. 開(kāi)始與暫停

  4. 選擇難度等級(jí)

  5. 設(shè)置快捷鍵

    5.1 通過(guò)ijkl,wsad也能實(shí)現(xiàn)方向的切換

    5.2 通過(guò)“P” 表示暫停,“C”表示開(kāi)始或繼續(xù),"R"表示重新開(kāi)始

實(shí)現(xiàn)過(guò)程

最開(kāi)始的實(shí)現(xiàn)原理其實(shí)是參考的csdn的一位大神,他用JavaScript20行就實(shí)現(xiàn)了貪吃蛇的基本功能,難等可貴的是還沒(méi)有bug,鏈接在此

要實(shí)現(xiàn)貪吃蛇大概有以下幾個(gè)步驟:

  • 畫(huà)一個(gè)蛇的移動(dòng)區(qū)域

  • 畫(huà)一條蛇

  • 畫(huà)食物

  • 讓蛇動(dòng)起來(lái)

  • 設(shè)定游戲規(guī)則

  • 設(shè)置難度等級(jí)

  • 設(shè)置開(kāi)始與暫停

  • 設(shè)置游戲結(jié)束后續(xù)操作

  • 實(shí)現(xiàn)人機(jī)交互頁(yè)面

注:下面的過(guò)程講解部分只是講述了部分原理與實(shí)現(xiàn),建議一邊看最后的完整代碼,一邊看下面的講解,更容易理解每一部分的原理與實(shí)現(xiàn)

畫(huà)蛇的活動(dòng)區(qū)域

首先我們畫(huà)蛇的活動(dòng)區(qū)域,我們采用JavaScript的canvas進(jìn)行繪制

我們用一個(gè)400 × 400 400\times 400400×400的區(qū)域作為蛇的活動(dòng)區(qū)域

<canvas id="canvas" width="400" height="400"></canvas>

同時(shí)通過(guò)CSS設(shè)置一個(gè)邊界線

#canvas {
    border: 1px solid #000000; /* 設(shè)置邊框線 */}
畫(huà)蛇和食物

效果如下:

如何使用JavaScript實(shí)現(xiàn)貪吃蛇小游戲

在畫(huà)蛇前我們需要想下蛇的數(shù)據(jù)結(jié)構(gòu),在這里我們采取最簡(jiǎn)單的隊(duì)列表示蛇

  • 隊(duì)首表示蛇頭位置,隊(duì)尾表示蛇尾位置

  • 我們將之前畫(huà)的                                  400                          ×                          400                                 400\times 400                    400×400區(qū)域劃分為400個(gè)                                  20                          ×                          20                                 20\times 20                    20×20的方塊,用這些方塊組成蛇,那么蛇所在方塊的位置的取值范圍就是0~399

    舉個(gè)例子:

    var snake=[42,41,40];

    上述代碼表示蛇所在的位置為42,41,40三個(gè)方塊,其中蛇頭為42,蛇尾為40

對(duì)于食物,我們可以用一個(gè)變量food存儲(chǔ)食物的位置即可,食物的取值范圍為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)部

我們接下來(lái)通過(guò)canvas進(jìn)行繪制

首先在js中獲取canvas組件

const canvas = document.getElementById("canvas");const ctx = canvas.getContext("2d");

然后寫(xiě)繪制函數(shù)用于繪制方格,繪制方格的時(shí)候注意我們預(yù)留1px作為邊框,即我們所畫(huà)的方格的邊長(zhǎng)為18,我們實(shí)際填充的是18 × 18 18\times 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),長(zhǎng),寬,這里為了美觀留了1px用于邊框
    ctx.fillRect(
        (seat % 20) * 20 + 1,
        Math.floor(seat / 20) * 20 + 1,
        18,
        18
    );}
讓蛇動(dòng)起來(lái)

我們要想讓蛇動(dòng)起來(lái),首先要規(guī)定蛇運(yùn)動(dòng)的方向,我們用一個(gè)變量direction來(lái)表示蛇運(yùn)動(dòng)的方向,其取值范圍為{1,-1,20,-20},1 表示向右,-1 表示向左,20 表示向下,-20 表示向上,運(yùn)動(dòng)時(shí)只需要將蛇頭的位置加上direction就可以表示新蛇頭的位置,這樣我們就可以表示蛇的運(yùn)動(dòng)了。

那么如何讓蛇動(dòng)起來(lái)呢,對(duì)于蛇的每次移動(dòng),我們需要完成下面幾個(gè)基本操作:

  1. 將蛇運(yùn)動(dòng)的下一個(gè)位置變成新蛇頭

    • 將下一個(gè)位置加入蛇隊(duì)列

    • 繪制下一個(gè)方塊為淺藍(lán)色

  2. 把舊蛇頭變成蛇身

    • 繪制舊蛇頭為淺灰色

  3. 刪除舊蛇尾

    • 將舊蛇尾彈出蛇隊(duì)列

    • 繪制舊蛇尾位置為白色

當(dāng)蛇吃掉食物時(shí)(蛇的下一個(gè)位置為食物所在位置)則需更新食物的位置,并繪制新食物為黃色,此時(shí)則不需要?jiǎng)h除舊蛇尾,這樣可以實(shí)現(xiàn)蛇吃完食物后長(zhǎng)度的增加功能

我們需要寫(xiě)一個(gè)函數(shù)實(shí)現(xiàn)上述操作,并且要不斷地調(diào)用這個(gè)函數(shù),從而實(shí)現(xiàn)頁(yè)面的刷新,即我們所說(shuō)的動(dòng)態(tài)效果

n = snake[0] + direction; // 找到新蛇頭坐標(biāo)snake.unshift(n); // 添加新蛇頭draw(n, "#1a8dcc"); // 繪制新蛇頭為淺藍(lán)色draw(snake[1], "#cececc"); // 將原來(lái)的蛇頭(淺藍(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"); // 將原來(lái)的蛇尾繪制成白色}

接下來(lái),我們需要實(shí)現(xiàn)通過(guò)鍵盤(pán)控制蛇的運(yùn)動(dòng)

我們需要獲取鍵盤(pán)的key值,然后通過(guò)一個(gè)監(jiān)聽(tīng)函數(shù)去監(jiān)聽(tīng)鍵盤(pán)按下的操作,我們這里通過(guò)上下左右鍵(還拓展了WSAD鍵和IJKL鍵控制上下左右方向),同時(shí)設(shè)置一個(gè)變量n表示下一步的方向

// 用于綁定鍵盤(pá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為其他值,即表示按了沒(méi)用的鍵,則方向不變
    } 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ī)則如下:

  1. 蛇如果撞到墻或者蛇的身體或尾巴則游戲結(jié)束

  2. 蛇如果吃掉食物則蛇的長(zhǎng)度會(huì)增加(上一步已經(jīng)實(shí)現(xiàn))且得分會(huì)增加

先實(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();}

接下來(lái)我們實(shí)現(xiàn)得分統(tǒng)計(jì),對(duì)于得分的計(jì)算我們只需要設(shè)置一個(gè)變量score,用于統(tǒng)計(jì)得分,然后每吃一個(gè)食物,該變量加一,然后將得分信息更新到網(wǎng)頁(yè)相應(yīng)位置

score = score + 1;score_cal.innerText = "目前得分: " + score; // 更新得分
設(shè)置難度等級(jí)

我們?cè)诰W(wǎng)頁(yè)上設(shè)置一個(gè)單選框,用于設(shè)置難度等級(jí)

<form action="" id="mode_form">
    難度等級(jí): 
    <input type="radio" name="mode" id="simply" value="simply" checked />
    <label for="simply">簡(jiǎn)單</label>
    <input type="radio" name="mode" id="middle" value="middle" />
    <label for="middle">中級(jí)</label>
    <input type="radio" name="mode" id="hard" value="hard" />
    <label for="hard">困難</label></form>

效果如下:

如何使用JavaScript實(shí)現(xiàn)貪吃蛇小游戲

那么我們后臺(tái)具體如何設(shè)置難度等級(jí)的功能呢?

我們采取調(diào)用蛇運(yùn)動(dòng)的函數(shù)的時(shí)間間隔來(lái)代替難度,時(shí)間間隔越小則難度越大,我們分三級(jí):簡(jiǎn)單、中級(jí)、困難

我們創(chuàng)建一個(gè)時(shí)間間隔變量time_internal,然后用一個(gè)函數(shù)獲取單選框的取值,并將相應(yīng)模式的時(shí)間間隔賦值給time_internal

// 用刷新間隔代表蛇的速度,刷新間隔越長(zhǎng),則蛇的速度越慢const simply_mode = 200;const middle_mode = 100;const hard_mode = 50;var time_internal = simply_mode; // 刷新時(shí)間間隔,用于調(diào)整蛇的速度,默認(rèn)為簡(jiǎn)單模式// 同步難度等級(jí)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;
    }}

最后只需要在蛇每次移動(dòng)前調(diào)用一次上述函數(shù)syncMode()就可以實(shí)現(xiàn)難度切換,至于蛇的速度的具體調(diào)節(jié)且看下面如何講解

設(shè)置開(kāi)始與暫停

如何實(shí)現(xiàn)蛇的移動(dòng)動(dòng)態(tài)效果,如何暫停,如何繼續(xù),速度如何調(diào)節(jié),這就涉及到JavaScript的動(dòng)畫(huà)的部分了,建議看下《JavaScript高級(jí)程序設(shè)計(jì)(第4版)》第18章的部分,講的很詳細(xì)。

在最初的“20行JavaScript實(shí)現(xiàn)貪吃蛇”中并沒(méi)有實(shí)現(xiàn)開(kāi)始與暫停,其實(shí)現(xiàn)動(dòng)態(tài)效果的方法為設(shè)置一個(gè)立即執(zhí)行函數(shù)!function() {}();,然后在該函數(shù)中使用setTimeout(arguments.callee, 150);,每隔0.15秒調(diào)用此函數(shù),從而實(shí)現(xiàn)了網(wǎng)頁(yè)的不斷刷新,也就是所謂的動(dòng)態(tài)效果。

后來(lái),我通過(guò)web課程老師的案例(彈球游戲)中了解到requestAnimationFrame方法可以實(shí)現(xiàn)動(dòng)畫(huà)效果,于是我便百度查詢(xún),最后在翻書(shū)《JavaScript高級(jí)程序設(shè)計(jì)(第4版)》第18章動(dòng)畫(huà)與Canvas圖形中得到啟發(fā)–如何實(shí)現(xiàn)開(kāi)始與取消,如何自定義時(shí)間間隔(實(shí)現(xiàn)難度調(diào)節(jié),蛇的速度)

書(shū)中給出的開(kāi)始動(dòng)畫(huà)與取消動(dòng)畫(huà)的方法如下:

注:為了便于理解,自己修改過(guò)原方法

var requestID; // 用于標(biāo)記請(qǐng)求ID與取消動(dòng)畫(huà)
function updateProgress() { 
	// do something...
    requestID = requestAnimationFrame(updateProgress); // 調(diào)用后在函數(shù)中反復(fù)調(diào)用該函數(shù)
} 
id = requestAnimationFrame(updateProgress); // 第一次調(diào)用(即開(kāi)始動(dòng)畫(huà))

cancelAnimationFrame(requestID); // 取消動(dòng)畫(huà)

書(shū)中講述道:

requestAnimationFrame()已經(jīng)解決了瀏覽器不知道 JavaScript 動(dòng)畫(huà)何時(shí)開(kāi)始的問(wèn)題, 以及最佳間隔是多少的問(wèn)題?!ぁぁぁぁぁ?/p>

傳給 requestAnimationFrame()的函數(shù)實(shí)際上可以接收一個(gè)參數(shù),此參數(shù)是一個(gè) DOMHighResTimeStamp 的實(shí)例(比如 performance.now()返回的值),表示下次重繪的時(shí)間。這一點(diǎn)非常重要: requestAnimationFrame()實(shí)際上把重繪任務(wù)安排在了未來(lái)一個(gè)已知的時(shí)間點(diǎn)上,而且通過(guò)這個(gè)參數(shù) 告訴了開(kāi)發(fā)者?;谶@個(gè)參數(shù),就可以更好地決定如何調(diào)優(yōu)動(dòng)畫(huà)了。

requestAnimationFrame()返回一個(gè)請(qǐng)求 ID,可以用于通過(guò)另一個(gè) 方法 cancelAnimationFrame()來(lái)取消重繪任務(wù)

書(shū)中同樣給出了如何控制時(shí)間間隔的方法:

書(shū)中講述道:

配合使用一個(gè)計(jì)時(shí)器來(lái)限制重繪操作執(zhí)行的頻率。這樣,計(jì)時(shí)器可以限制實(shí)際的操作執(zhí)行間隔,而 requestAnimationFrame 控制在瀏覽器的哪個(gè)渲染周期中執(zhí)行。下面的例子可以將回調(diào)限制為不超過(guò) 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)動(dòng)的函數(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);}// 啟動(dòng)或繼續(xù)游戲function run_game() {
    syncMode(); // 同步難度等級(jí)
    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ái)的蛇頭(淺藍(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"); // 將原來(lái)的蛇尾繪制成白色
    }
    // setTimeout(arguments.callee, time_internal); //之前的方案,無(wú)法實(shí)現(xiàn)暫停和游戲的繼續(xù)}

至于暫停只需要在特定的位置調(diào)用cancelAnimationFrame(run_id);就可以了

設(shè)置游戲結(jié)束后續(xù)操作

我想的是在游戲結(jié)束后出現(xiàn)一個(gè)“彈窗”,顯示最終得分和是否再來(lái)一把

效果如下:

如何使用JavaScript實(shí)現(xiàn)貪吃蛇小游戲

首先,我們實(shí)現(xiàn)網(wǎng)頁(yè)的彈窗,通過(guò)調(diào)研發(fā)現(xiàn)JavaScript的彈窗可以通過(guò)alert()的方法實(shí)現(xiàn),不過(guò)在網(wǎng)頁(yè)上直接彈窗感覺(jué)不太美觀,而且影響體驗(yàn),于是我想了一下,可以采用一個(gè)p標(biāo)簽實(shí)現(xiàn)偽彈窗,在需要顯示的時(shí)候設(shè)置其display屬性為block,不需要顯示的時(shí)候設(shè)置其display屬性為none,就類(lèi)似于Photoshop里面的圖層概念,這樣我們就可以在平常的時(shí)候設(shè)置其display屬性為none觸發(fā)game over時(shí)設(shè)置其display屬性為block,實(shí)現(xiàn)如下:

<p id="game_over">
    <h4 id="game_over_text" align="center">游戲結(jié)束!</h4>
    <h4 id="game_over_score" align="center">您的最終得分為: 0分</h4>
    <button id="once_again">再來(lái)一把</button>
    <button id="cancel">取消</button></p>

其CSS部分如下:

#game_over {
    display: none; /* 設(shè)置game over 窗口不可見(jiàn) */
    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;}

接下來(lái),我們需要實(shí)現(xiàn)game over的后續(xù)操作:暫停動(dòng)畫(huà),顯示得分,顯示“彈窗”

function game_over(){
    cancelAnimationFrame(run_id);
    game_over_score.innerText = "您的最終得分為: " + score + "分";
    game_over_p.style.display = "block";}
實(shí)現(xiàn)人機(jī)交互頁(yè)面

接下來(lái)的部分就是提高用戶體驗(yàn)的部分,具體實(shí)現(xiàn)下列功能/操作

  1. 游戲說(shuō)明

  2. 人機(jī)交互按鈕:開(kāi)始/繼續(xù),暫停,重新開(kāi)始

  3. 快捷鍵

    • 由于在游戲過(guò)程中通過(guò)鼠標(biāo)移動(dòng)到暫停鍵暫停,時(shí)間上太慢,可能造成游戲終止,故應(yīng)該設(shè)置開(kāi)始/繼續(xù)(C)、暫停(P)、重新開(kāi)始(R)的快捷鍵

    • 有些電腦鍵盤(pán)的上下左右鍵較小,操作起來(lái)不太方便,可以添加WSAD或者IJKL擴(kuò)展,用于控制上下左右方向

效果如下:

如何使用JavaScript實(shí)現(xiàn)貪吃蛇小游戲

至于寫(xiě)界面的代碼,可以看文末的完整代碼,這里就稍微講解下綁定按鍵點(diǎn)擊事件與綁定快捷鍵

我們首先看下綁定按鍵點(diǎn)擊事件,點(diǎn)擊”開(kāi)始/繼續(xù)“只需要調(diào)用requestAnimationFrame(game_control);,點(diǎn)擊”暫?!爸恍枰{(diào)用cancelAnimationFrame(run_id);

// 綁定開(kāi)始按鈕點(diǎn)擊事件start_btn.onclick = function () {
    run_id = requestAnimationFrame(game_control);};// 綁定暫停按鈕點(diǎn)擊事件pause_btn.onclick = function () {
    cancelAnimationFrame(run_id);};

點(diǎn)擊“重新開(kāi)始”的話,則需要先暫停動(dòng)畫(huà),然后刪除畫(huà)面上的蛇和食物,初始化所有設(shè)置,然后再調(diào)用requestAnimationFrame(game_control);開(kāi)始游戲

注:初始化時(shí)需要初始化得分與難度等級(jí),這里解釋下為什么要將第一個(gè)食物設(shè)置為蛇頭下一個(gè)位置,因?yàn)檫@樣的話蛇會(huì)自動(dòng)先吃一個(gè)食物,繼而可以通過(guò)“開(kāi)始 / 繼續(xù)” 一個(gè)按鈕實(shí)現(xiàn)開(kāi)始和繼續(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; // 重置難度等級(jí)為簡(jiǎn)單}// 綁定重新開(kāi)始按鈕點(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);			};

接下來(lái),我們綁定game over中的兩個(gè)按鍵”再來(lái)一把“和”取消“

”再來(lái)一把“只需要完成“重新開(kāi)始”里面的事件即可,”取消“只需要完成”重新開(kāi)始“點(diǎn)擊操作中除了開(kāi)始游戲的部分,即除了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í)的再來(lái)一把按鈕點(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);}

最后,我們來(lái)講解下如何設(shè)置快捷鍵,快捷鍵只需要用JavaScript模擬點(diǎn)擊對(duì)應(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();
    } };

問(wèn)題、調(diào)試與解決

注: 此部分為本人在實(shí)現(xiàn)過(guò)程中出現(xiàn)的bug、調(diào)試過(guò)程以及解決方法,感興趣的可以看看,不感興趣的也可以跳過(guò)此部分,直接看文末的完整代碼

問(wèn)題1:點(diǎn)擊暫停和開(kāi)始,游戲正常開(kāi)始,按P也可以實(shí)現(xiàn)暫停,按C則畫(huà)面出現(xiàn)蛇所在的方格亂跳,無(wú)法正常開(kāi)始,但是按C的操作中只模擬了”開(kāi)始 / 繼續(xù)“按鈕的點(diǎn)擊?

效果如下:

如何使用JavaScript實(shí)現(xiàn)貪吃蛇小游戲

調(diào)試過(guò)程:因?yàn)樯哳^的位置是由direction控制的,故想到設(shè)置斷點(diǎn),同時(shí)監(jiān)測(cè)這個(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í)行完對(duì)應(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; // 后來(lái)加上的
    } else if(keycode == 80){
        // P 暫停
        pause_btn.onclick();
        return; // 后來(lái)加上的
    } else if(keycode == 67){
        // C 繼續(xù)
        start_btn.onclick();
        return; // 后來(lái)加上的
    } 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; // 若方向與原方向相反,則方向不變};

問(wèn)題2:調(diào)整難度等級(jí)后,蛇的速度并沒(méi)有發(fā)生改變,但是通過(guò)console.log()發(fā)現(xiàn)確實(shí)調(diào)用了同步難度模式的函數(shù)?

調(diào)試過(guò)程:在同步難度等級(jí)的函數(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;

原代碼和修改后的代碼如下:

// 同步難度等級(jí)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;//原來(lái)是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 窗口不可見(jiàn) */
		  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>
    <h2 id="game_title">貪吃蛇小游戲</h2>
    <canvas id="canvas" width="400" height="400"></canvas>
	<p id="game_over">
		<h4 id="game_over_text" align="center">游戲結(jié)束!</h4>
		<h4 id="game_over_score" align="center">您的最終得分為: 0分</h4>
		<button id="once_again">再來(lái)一把</button>
		<button id="cancel">取消</button>
	</p>
	<br>
	<p id="game_info">
		<p><b>游戲說(shuō)明:</b></p>
		<p>
			<b>1</b>. 用鍵盤(pán)上下左右鍵(或者IJKL鍵,或者WSAD鍵)控制蛇的方向,尋找吃的東西		<br><b>2</b>. 每吃一口就能得到一定的積分,同時(shí)蛇的身子會(huì)越吃越長(zhǎng)		<br><b>3</b>. 不能碰墻,不能咬到自己的身體,更不能咬自己的尾巴		<br><b>4</b>. 在下方單選框中選擇難度等級(jí),點(diǎn)擊"<b>開(kāi)始 / 繼續(xù)</b>"即開(kāi)始游戲,點(diǎn)擊"<b>暫停</b>"則暫停游戲,			<br>&nbsp;&nbsp;&nbsp;&nbsp;再點(diǎn)擊"<b>開(kāi)始 / 繼續(xù)</b>"繼續(xù)游戲,點(diǎn)擊"重新開(kāi)始"則重新開(kāi)始游戲		<br><b>5</b>. <b>快捷鍵</b>: "<b>C</b>"表示開(kāi)始或繼續(xù),"<b>P</b>"表示暫停,"<b>R</b>"表示重新開(kāi)始		</p>
	</p>
    
    <p id="score">目前得分: 0分</p>
    <form action="" id="mode_form">
      難度等級(jí): 
      <input type="radio" name="mode" id="simply" value="simply" checked />
      <label for="simply">簡(jiǎn)單</label>
      <input type="radio" name="mode" id="middle" value="middle" />
      <label for="middle">中級(jí)</label>
      <input type="radio" name="mode" id="hard" value="hard" />
      <label for="hard">困難</label>
    </form>
    <br />
    <button id="startButton">開(kāi)始 / 繼續(xù)</button>
    <button id="pauseButton">暫停</button>
    <button id="restartButton">重新開(kāi)始</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");

		// 用刷新間隔代表蛇的速度,刷新間隔越長(zhǎng),則蛇的速度越慢
		const simply_mode = 200;
		const middle_mode = 100;
		const hard_mode = 50;

		//注意要改為var const是不會(huì)修改的
		var snake = [41, 40]; // 蛇身體隊(duì)列
		var direction = 1; // 方向:1為向右,-1為向左,20為向下,-20為向上
		var food = 42; // 食物位置,取值為0~399
		var n; // 蛇的下一步的方向(由鍵盤(pán)和蛇的原方向決定)
		var score = -1; // 得分
		var time_internal = simply_mode; // 刷新時(shí)間間隔,用于調(diào)整蛇的速度,默認(rèn)為簡(jiǎn)單模式

		let enabled = true; // 用于控制是否刷新,實(shí)現(xiàn)通過(guò)一定頻率刷新
		let run_id; // 請(qǐng)求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),長(zhǎng),寬,這里為了美觀留了1px用于邊框
			ctx.fillRect(
				(seat % 20) * 20 + 1,
				Math.floor(seat / 20) * 20 + 1,
				18,
				18
			);
		}

		// 同步難度等級(jí)
		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;//原來(lái)是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;
			}
		}

		// 用于綁定鍵盤(pán)上下左右事件,我設(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; // 重置難度等級(jí)為簡(jiǎn)單
		}

		function game_over(){
			cancelAnimationFrame(run_id);
			game_over_score.innerText = "您的最終得分為: " + score + "分";
			game_over_p.style.display = "block";
		}

		// 啟動(dòng)或繼續(xù)游戲
		function run_game() {
			syncMode(); // 同步難度等級(jí)
			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ái)的蛇頭(淺藍(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"); // 將原來(lái)的蛇尾繪制成白色
			}
			// setTimeout(arguments.callee, time_internal); //之前的方案,無(wú)法實(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);
		}

		// 綁定開(kāi)始按鈕點(diǎn)擊事件
		start_btn.onclick = function () {
			run_id = requestAnimationFrame(game_control);
		};

		// 綁定暫停按鈕點(diǎn)擊事件
		pause_btn.onclick = function () {
			cancelAnimationFrame(run_id);
		};

		// 綁定重新開(kāi)始按鈕點(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í)的再來(lái)一把按鈕點(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>

關(guān)于“如何使用JavaScript實(shí)現(xiàn)貪吃蛇小游戲”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,使各位可以學(xué)到更多知識(shí),如果覺(jué)得文章不錯(cuò),請(qǐng)把它分享出去讓更多的人看到。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI