主頁 > 前端設計 > JavaScript實作貪吃蛇小游戲

JavaScript實作貪吃蛇小游戲

2021-11-07 08:39:38 前端設計

JavaScript實作貪吃蛇小游戲

功能概述

本程式實作了如下功能:

  1. 貪吃蛇的基本功能

  2. 統計得分

  3. 開始與暫停

  4. 選擇難度等級

  5. 設定快捷鍵

    5.1 通過ijkl,wsad也能實作方向的切換

    5.2 通過“P” 表示暫停,“C”表示開始或繼續,"R"表示重新開始

實作程序

最開始的實作原理其實是參考的csdn的一位大神,他用JavaScript20行就實作了貪吃蛇的基本功能,難等可貴的是還沒有bug,鏈接在此

要實作貪吃蛇大概有以下幾個步驟:

  1. 畫一個蛇的移動區域
  2. 畫一條蛇
  3. 畫食物
  4. 讓蛇動起來
  5. 設定游戲規則
  6. 設定難度等級
  7. 設定開始與暫停
  8. 設定游戲結束后續操作
  9. 實作人機互動頁面

注:下面的程序講解部分只是講述了部分原理與實作,建議一邊看最后的完整代碼,一邊看下面的講解,更容易理解每一部分的原理與實作

畫蛇的活動區域

首先我們畫蛇的活動區域,我們采用JavaScript的canvas進行繪制

我們用一個 400 × 400 400\times 400 400×400的區域作為蛇的活動區域

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

同時通過CSS設定一個邊界線

#canvas {
    border: 1px solid #000000; /* 設定邊框線 */
}

畫蛇和食物

效果如下:

image-20211105183534687

在畫蛇前我們需要想下蛇的資料結構,在這里我們采取最簡單的隊串列示蛇

  • 隊首表示蛇頭位置,隊尾表示蛇尾位置

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

    舉個例子:

    var snake=[42,41,40];
    

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

對于食物,我們可以用一個變數food存盤食物的位置即可,食物的取值范圍為0~399,且不包括蛇的部分,由于游戲中需要隨機產生食物,隨機函式實作如下:

// 產生min~max的隨機整數,用于隨機產生食物的位置
function random(min, max) {
    const num = Math.floor(Math.random() * (max - min)) + min;
    return num;
}

當食物被蛇吃掉后就需要重新重繪食物,由于食物不能出現在蛇所在的位置,我們用一個while回圈,當食物的位置不在蛇的陣列中則跳出回圈

while (snake.indexOf((food = random(0, 400))) >= 0); // 重新重繪食物,注意食物應不在蛇內部

我們接下來通過canvas進行繪制

首先在js中獲取canvas組件

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

然后寫繪制函式用于繪制方格,繪制方格的時候注意我們預留1px作為邊框,即我們所畫的方格的邊長為18,我們實際填充的是 18 × 18 18\times 18 18×18的方塊,方塊的x、y坐標(方塊的左上角)的計算也需要注意加上1px

注:canvas的原點坐標在左上角,往右為x軸正方向,往下為y軸正方向

// 用于繪制蛇或者是食物代表的方塊,seat為方塊位置,取值為0~399,color為顏色
function draw(seat, color) {
    ctx.fillStyle = color; // 填充顏色
    // fillRect的四個引數分別表示要繪制方塊的x坐標,y坐標,長,寬,這里為了美觀留了1px用于邊框
    ctx.fillRect(
        (seat % 20) * 20 + 1,
        Math.floor(seat / 20) * 20 + 1,
        18,
        18
    );
}

讓蛇動起來

我們要想讓蛇動起來,首先要規定蛇運動的方向,我們用一個變數direction來表示蛇運動的方向,其取值范圍為{1,-1,20,-20},1 表示向右,-1 表示向左,20 表示向下,-20 表示向上,運動時只需要將蛇頭的位置加上direction就可以表示新蛇頭的位置,這樣我們就可以表示蛇的運動了,

那么如何讓蛇動起來呢,對于蛇的每次移動,我們需要完成下面幾個基本操作:

  1. 將蛇運動的下一個位置變成新蛇頭
    • 將下一個位置加入蛇佇列
    • 繪制下一個方塊為淺藍色
  2. 把舊蛇頭變成蛇身
    • 繪制舊蛇頭為淺灰色
  3. 洗掉舊蛇尾
    • 將舊蛇尾彈出蛇佇列
    • 繪制舊蛇尾位置為白色

當蛇吃掉食物時(蛇的下一個位置為食物所在位置)則需更新食物的位置,并繪制新食物為黃色,此時則不需要洗掉舊蛇尾,這樣可以實作蛇吃完食物后長度的增加功能

我們需要寫一個函式實作上述操作,并且要不斷地呼叫這個函式,從而實作頁面的重繪,即我們所說的動態效果

n = snake[0] + direction; // 找到新蛇頭坐標
snake.unshift(n); // 添加新蛇頭
draw(n, "#1a8dcc"); // 繪制新蛇頭為淺藍色
draw(snake[1], "#cececc"); // 將原來的蛇頭(淺藍色)變成蛇身(淺灰色)
if (n == food) {
    while (snake.indexOf((food = random(0, 400))) >= 0); // 重新重繪食物,注意食物應不在蛇內部
    draw(food, "Yellow"); // 繪制食物
} else {
    draw(snake.pop(), "White"); // 將原來的蛇尾繪制成白色
}

接下來,我們需要實作通過鍵盤控制蛇的運動

我們需要獲取鍵盤的key值,然后通過一個監聽函式去監聽鍵盤按下的操作,我們這里通過上下左右鍵(還拓展了WSAD鍵和IJKL鍵控制上下左右方向),同時設定一個變數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; // 若方向與原方向相反,則方向不變
};

設定游戲規則

貪吃蛇的最基礎的游戲規則如下:

  1. 蛇如果撞到墻或者蛇的身體或尾巴則游戲結束
  2. 蛇如果吃掉食物則蛇的長度會增加(上一步已經實作)且得分會增加

先實作第一個,具體如下:

注:下面的一段代碼中的n即為新蛇頭的位置

// 判斷蛇頭是否撞到自己或者是否超出邊界
if (
    snake.indexOf(n, 1) > 0 ||
    n < 0 ||
    n > 399 ||
    (direction == 1 && n % 20 == 0) ||
    (direction == -1 && n % 20 == 19)
) {
    game_over();
}

接下來我們實作得分統計,對于得分的計算我們只需要設定一個變數score,用于統計得分,然后每吃一個食物,該變數加一,然后將得分資訊更新到網頁相應位置

score = score + 1;
score_cal.innerText = "目前得分: " + score; // 更新得分

設定難度等級

我們在網頁上設定一個單選框,用于設定難度等級

<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>

效果如下:

image-20211105193208392

那么我們后臺具體如何設定難度等級的功能呢?

我們采取呼叫蛇運動的函式的時間間隔來代替難度,時間間隔越小則難度越大,我們分三級:簡單、中級、困難

我們創建一個時間間隔變數time_internal,然后用一個函式獲取單選框的取值,并將相應模式的時間間隔賦值給time_internal

// 用重繪間隔代表蛇的速度,重繪間隔越長,則蛇的速度越慢
const simply_mode = 200;
const middle_mode = 100;
const hard_mode = 50;

var time_internal = simply_mode; // 重繪時間間隔,用于調整蛇的速度,默認為簡單模式

// 同步難度等級
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;
    }
}

最后只需要在蛇每次移動前呼叫一次上述函式syncMode()就可以實作難度切換,至于蛇的速度的具體調節且看下面如何講解

設定開始與暫停

如何實作蛇的移動動態效果,如何暫停,如何繼續,速度如何調節,這就涉及到JavaScript的影片的部分了,建議看下《JavaScript高級程式設計(第4版)》第18章的部分,講的很詳細,

在最初的“20行JavaScript實作貪吃蛇”中并沒有實作開始與暫停,其實作動態效果的方法為設定一個立即執行函式!function() {}();,然后在該函式中使用setTimeout(arguments.callee, 150);,每隔0.15秒呼叫此函式,從而實作了網頁的不斷重繪,也就是所謂的動態效果,

后來,我通過web課程老師的案例(彈球游戲)中了解到requestAnimationFrame方法可以實作影片效果,于是我便百度查詢,最后在翻書《JavaScript高級程式設計(第4版)》第18章影片與Canvas圖形中得到啟發–如何實作開始與取消,如何自定義時間間隔(實作難度調節,蛇的速度)

書中給出的開始影片與取消影片的方法如下:

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

var requestID; // 用于標記請求ID與取消影片
function updateProgress() { 
	// do something...
    requestID = requestAnimationFrame(updateProgress); // 呼叫后在函式中反復呼叫該函式
} 
id = requestAnimationFrame(updateProgress); // 第一次呼叫(即開始影片)

cancelAnimationFrame(requestID); // 取消影片

書中講述道:

requestAnimationFrame()已經解決了瀏覽器不知道 JavaScript 影片何時開始的問題, 以及最佳間隔是多少的問題,······

傳給 requestAnimationFrame()的函式實際上可以接收一個引數,此引數是一個 DOMHighResTimeStamp 的實體(比如 performance.now()回傳的值),表示下次重繪的時間,這一點非常重要: requestAnimationFrame()實際上把重繪任務安排在了未來一個已知的時間點上,而且通過這個引數 告訴了開發者,基于這個引數,就可以更好地決定如何調優影片了,

requestAnimationFrame()回傳一個請求 ID,可以用于通過另一個 方法 cancelAnimationFrame()來取消重繪任務

書中同樣給出了如何控制時間間隔的方法:

書中講述道:

配合使用一個計時器來限制重繪操作執行的頻率,這樣,計時器可以限制實際的操作執行間隔,而 requestAnimationFrame 控制在瀏覽器的哪個渲染周期中執行,下面的例子可以將回呼限制為不超過 50 毫秒執行一次

具體方法如下:

let enabled = true; 
function expensiveOperation() { 
	console.log('Invoked at', Date.now()); 
} 
window.addEventListener('scroll', () => { 
 if (enabled) { 
     enabled = false; 
     requestAnimationFrame(expensiveOperation); 
     setTimeout(() => enabled = true, 50); 
 } 
}); 

由上面的方法我得到啟發,在此處我們可以設定一個控制函式,用于控制隔一定的時間呼叫一次蛇運動的函式,實作如下:

// 控制游戲的重繪頻率,每隔time_internal時間間隔重繪一次
function game_control(){
    if(enabled){
        enabled = false;
        requestAnimationFrame(run_game);
        setTimeout(() => enabled = true, time_internal);
    }
    run_id = requestAnimationFrame(game_control);
}

// 啟動或繼續游戲
function run_game() {
    syncMode(); // 同步難度等級
    n = snake[0] + direction; // 找到新蛇頭坐標
    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"); // 繪制新蛇頭為淺藍色
    draw(snake[1], "#cececc"); // 將原來的蛇頭(淺藍色)變成蛇身(淺灰色)
    if (n == food) {
        score = score + 1;
        score_cal.innerText = "目前得分: " + score; // 更新得分
        while (snake.indexOf((food = random(0, 400))) >= 0); // 重新重繪食物,注意食物應不在蛇內部
        draw(food, "Yellow"); // 繪制食物
    } else {
        draw(snake.pop(), "White"); // 將原來的蛇尾繪制成白色
    }
    // setTimeout(arguments.callee, time_internal); //之前的方案,無法實作暫停和游戲的繼續
}

至于暫停只需要在特定的位置呼叫cancelAnimationFrame(run_id);就可以了

設定游戲結束后續操作

我想的是在游戲結束后出現一個“彈窗”,顯示最終得分和是否再來一把

效果如下:

image-20211105203816613

首先,我們實作網頁的彈窗,通過調研發現JavaScript的彈窗可以通過alert()的方法實作,不過在網頁上直接彈窗感覺不太美觀,而且影響體驗,于是我想了一下,可以采用一個div標簽實作偽彈窗,在需要顯示的時候設定其display屬性為block,不需要顯示的時候設定其display屬性為none,就類似于Photoshop里面的圖層概念,這樣我們就可以在平常的時候設定其display屬性為none觸發game over時設定其display屬性為block,實作如下:

<div id="game_over">
    <h3 id="game_over_text" align="center">游戲結束!</h3>
    <h3 id="game_over_score" align="center">您的最終得分為: 0分</h3>
    <button id="once_again">再來一把</button>
    <button id="cancel">取消</button>
</div>

其CSS部分如下:

#game_over {
    display: none; /* 設定game over 視窗不可見 */
    position: fixed;
    top: 190px;
    left: 65px;
    width: 280px;
    height: 160px;
    background-color: aliceblue;
    border-radius: 5px;
    border: 1px solid #000; /* 設定邊框線 */
}
#once_again {
    position: relative;
    left: 20px;
}
#cancel {
    position: relative;
    left: 50px;
}

接下來,我們需要實作game over的后續操作:暫停影片,顯示得分,顯示“彈窗”

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

實作人機互動頁面

接下來的部分就是提高用戶體驗的部分,具體實作下列功能/操作

  1. 游戲說明
  2. 人機互動按鈕:開始/繼續,暫停,重新開始
  3. 快捷鍵
    • 由于在游戲程序中通過滑鼠移動到暫停鍵暫停,時間上太慢,可能造成游戲終止,故應該設定開始/繼續(C)、暫停(P)、重新開始(R)的快捷鍵
    • 有些電腦鍵盤的上下左右鍵較小,操作起來不太方便,可以添加WSAD或者IJKL擴展,用于控制上下左右方向

效果如下:

image-20211105205627819

至于寫界面的代碼,可以看文末的完整代碼,這里就稍微講解下系結按鍵點擊事件與系結快捷鍵

我們首先看下系結按鍵點擊事件,點擊”開始/繼續“只需要呼叫requestAnimationFrame(game_control);,點擊”暫停“只需要呼叫cancelAnimationFrame(run_id);

// 系結開始按鈕點擊事件
start_btn.onclick = function () {
    run_id = requestAnimationFrame(game_control);
};

// 系結暫停按鈕點擊事件
pause_btn.onclick = function () {
    cancelAnimationFrame(run_id);
};

點擊“重新開始”的話,則需要先暫停影片,然后洗掉畫面上的蛇和食物,初始化所有設定,然后再呼叫requestAnimationFrame(game_control);開始游戲

注:初始化時需要初始化得分與難度等級,這里解釋下為什么要將第一個食物設定為蛇頭下一個位置,因為這樣的話蛇會自動先吃一個食物,繼而可以通過“開始 / 繼續” 一個按鈕實作開始和繼續操作,同時run_game()函式中的食物繪制是在蛇吃到食物之后,保證第一個食物順利繪制,這樣的話score就需要初始化為-1

// 用于初始化游戲各項引數
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; // 重置難度等級為簡單
}

// 系結重新開始按鈕點擊事件
restart_btn.onclick = function () {
    cancelAnimationFrame(run_id);
    // 將原有的食物和蛇的方塊都繪制成白色
    for(var i = 0; i < snake.length; i++){
        draw(snake[i], "White");
    }
    draw(food, "White");
    // 初始化游戲各項引數
    init_game();
    run_id = requestAnimationFrame(game_control);			
};

接下來,我們系結game over中的兩個按鍵”再來一把“和”取消“

”再來一把“只需要完成“重新開始”里面的事件即可,”取消“只需要完成”重新開始“點擊操作中除了開始游戲的部分,即除了run_id = requestAnimationFrame(game_control);

這兩個按鈕都需要設定”彈窗“的display屬性為none

具體實作如下:

// 系結游戲結束時的取消按鈕點擊事件
cancel_btn.onclick = function () {
    for(var i = 0; i < snake.length; i++){
        draw(snake[i], "White");
    }
    draw(food, "White");
    init_game();
    game_over_div.style.display = "none";
}

// 系結游戲結束時的再來一把按鈕點擊事件
once_again_btn.onclick = function () {
    for(var i = 0; i < snake.length; i++){
        draw(snake[i], "White");
    }
    draw(food, "White");
    init_game();
    game_over_div.style.display = "none";
    run_id = requestAnimationFrame(game_control);
}

最后,我們來講解下如何設定快捷鍵,快捷鍵只需要用JavaScript模擬點擊對應的按鈕即可,實作如下:

// 同時系結R 重啟,P 暫停,C 繼續
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 繼續
        start_btn.onclick();
    } 
};

問題、除錯與解決

注: 此部分為本人在實作程序中出現的bug、除錯程序以及解決方法,感興趣的可以看看,不感興趣的也可以跳過此部分,直接看文末的完整代碼

問題1:點擊暫停和開始,游戲正常開始,按P也可以實作暫停,按C則畫面出現蛇所在的方格亂跳,無法正常開始,但是按C的操作中只模擬了”開始 / 繼續“按鈕的點擊?

效果如下:

image-20211105212956276

在這里特別鳴謝:@wallacewen

除錯程序:因為蛇頭的位置是由direction控制的,故想到設定斷點,同時監測這個變數的值的變化,發現這個值在按完P和C時被更新成很大的數,進而去找direction在哪里被更新,發現點擊P或C后還需要執行下面這一行代碼,而實際上是不需要的

direction = snake[1] - snake[0] == n ? direction : n; // 若方向與原方向相反,則方向不變

解決方法:只需要執行完對應的模擬滑鼠點擊相應按鈕事件之后就直接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 繼續
        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:調整難度等級后,蛇的速度并沒有發生改變,但是通過console.log()發現確實呼叫了同步難度模式的函式?

除錯程序:在同步難度等級的函式中設定console.log()方法,輸出time_internal變數,同時設斷點除錯,發現time_internal變數不發生變化,mode_value變數始終為undefined,最后發現應該是值傳遞時的錯誤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; /* 設定邊框線 */
		}
		#score {
		  font-weight: bold;
		}
		#mode_form {
		  font-weight: bold;
		}
		#game_over {
		  display: none; /* 設定game over 視窗不可見 */
		  position: fixed;
		  top: 190px;
		  left: 65px;
		  width: 280px;
		  height: 160px;
		  background-color: aliceblue;
		  border-radius: 5px;
		  border: 1px solid #000; /* 設定邊框線 */
		}
		#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>
	<div id="game_over">
		<h3 id="game_over_text" align="center">游戲結束!</h3>
		<h3 id="game_over_score" align="center">您的最終得分為: 0分</h3>
		<button id="once_again">再來一把</button>
		<button id="cancel">取消</button>
	</div>
	<br>
	<div id="game_info">
		<p><b>游戲說明:</b></p>
		<p>
			<b>1</b>. 用鍵盤上下左右鍵(或者IJKL鍵,或者WSAD鍵)控制蛇的方向,尋找吃的東西
		<br><b>2</b>. 每吃一口就能得到一定的積分,同時蛇的身子會越吃越長
		<br><b>3</b>. 不能碰墻,不能咬到自己的身體,更不能咬自己的尾巴
		<br><b>4</b>. 在下方單選框中選擇難度等級,點擊"<b>開始 / 繼續</b>"即開始游戲,點擊"<b>暫停</b>"則暫停游戲,
			<br>&nbsp;&nbsp;&nbsp;&nbsp;再點擊"<b>開始 / 繼續</b>"繼續游戲,點擊"重新開始"則重新開始游戲
		<br><b>5</b>. <b>快捷鍵</b>: "<b>C</b>"表示開始或繼續,"<b>P</b>"表示暫停,"<b>R</b>"表示重新開始
		</p>
	</div>
    
    <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">開始 / 繼續</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_div = 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]; // 蛇身體佇列
		var direction = 1; // 方向:1為向右,-1為向左,20為向下,-20為向上
		var food = 42; // 食物位置,取值為0~399
		var n; // 蛇的下一步的方向(由鍵盤和蛇的原方向決定)
		var score = -1; // 得分
		var time_internal = simply_mode; // 重繪時間間隔,用于調整蛇的速度,默認為簡單模式

		let enabled = true; // 用于控制是否重繪,實作通過一定頻率重繪
		let run_id; // 請求ID,用于暫停功能

		// 產生min~max的隨機整數,用于隨機產生食物的位置
		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的四個引數分別表示要繪制方塊的x坐標,y坐標,長,寬,這里為了美觀留了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;
			}
		}

		// 用于系結鍵盤上下左右事件,我設定了wsad,或者ijkl,或者上下左右方向鍵,代表上下左右方向
		// 同時系結R 重啟,P 暫停,C 繼續,注意:若是這幾個鍵則不需要更新direction的值,操作結束后直接回傳即可
		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 繼續
				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; // 若方向與原方向相反,則方向不變
		};

		// 用于初始化游戲各項引數
		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_div.style.display = "block";
		}

		// 啟動或繼續游戲
		function run_game() {
			syncMode(); // 同步難度等級
			n = snake[0] + direction; // 找到新蛇頭坐標
			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"); // 繪制新蛇頭為淺藍色
			draw(snake[1], "#cececc"); // 將原來的蛇頭(淺藍色)變成蛇身(淺灰色)
			if (n == food) {
				score = score + 1;
				score_cal.innerText = "目前得分: " + score; // 更新得分
				while (snake.indexOf((food = random(0, 400))) >= 0); // 重新重繪食物,注意食物應不在蛇內部
				draw(food, "Yellow"); // 繪制食物
			} else {
				draw(snake.pop(), "White"); // 將原來的蛇尾繪制成白色
			}
			// setTimeout(arguments.callee, time_internal); //之前的方案,無法實作暫停和游戲的繼續
		}

		// 控制游戲的重繪頻率,每隔time_internal時間間隔重繪一次
		function game_control(){
			if(enabled){
				enabled = false;
				requestAnimationFrame(run_game);
				setTimeout(() => enabled = true, time_internal);
			}
			run_id = requestAnimationFrame(game_control);
		}

		// 系結開始按鈕點擊事件
		start_btn.onclick = function () {
			run_id = requestAnimationFrame(game_control);
		};

		// 系結暫停按鈕點擊事件
		pause_btn.onclick = function () {
			cancelAnimationFrame(run_id);
		};

		// 系結重新開始按鈕點擊事件
		restart_btn.onclick = function () {
			cancelAnimationFrame(run_id);
			// 將原有的食物和蛇的方塊都繪制成白色
			for(var i = 0; i < snake.length; i++){
				draw(snake[i], "White");
			}
			draw(food, "White");
			// 初始化游戲各項引數
			init_game();
			run_id = requestAnimationFrame(game_control);			
		};

		// 系結游戲結束時的取消按鈕點擊事件
		cancel_btn.onclick = function () {
			for(var i = 0; i < snake.length; i++){
				draw(snake[i], "White");
			}
			draw(food, "White");
			init_game();
			game_over_div.style.display = "none";
		}

		// 系結游戲結束時的再來一把按鈕點擊事件
		once_again_btn.onclick = function () {
			for(var i = 0; i < snake.length; i++){
				draw(snake[i], "White");
			}
			draw(food, "White");
			init_game();
			game_over_div.style.display = "none";
			run_id = requestAnimationFrame(game_control);
		}
    </script>
  </body>
</html>

轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/350989.html

標籤:其他

上一篇:計算機不理解可以報錯 我不理解是真的不理解呀~

下一篇:為什么必須在XML屬性中對<進行轉義?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • vue移動端上拉加載

    可能做得過于簡單或者比較low,請各位大佬留情,一起探討技術 ......

    uj5u.com 2020-09-10 04:38:07 more
  • 優美網站首頁,頂部多層導航

    一個個人用的瀏覽器首頁,可以把一下常用的網站放在這里,平常打開會比較方便。 第一步,HTML代碼 <script src=https://www.cnblogs.com/szharf/p/"js/jquery-3.4.1.min.js"></script> <div id="navigate"> <ul> <li class="labels labels_1"> ......

    uj5u.com 2020-09-10 04:38:47 more
  • 頁面為要加<!DOCTYPE html>

    最近因為寫一個js函式,需要用到$(window).height(); 由于手寫demo的時候,過于自信,其實對前端方面的認識也不夠體系,用文本檔案直接敲出來的html代碼,第一行沒有加上<!DOCTYPE html> 導致了$(window).height();的結果直接是整個document的高 ......

    uj5u.com 2020-09-10 04:38:52 more
  • WordPress網站程式手動升級要做好資料備份

    WordPress博客網站程式在進行升級前,必須要做好網站資料的備份,這個問題良家佐言是遇見過的;在剛開始接觸WordPress博客程式的時候,因為升級問題和博客網站的修改的一些嘗試,良家佐言是吃盡了苦頭。因為購買的是西部數碼的空間和域名,每當佐言把自己的WordPress博客網站搞到一塌糊涂的時候 ......

    uj5u.com 2020-09-10 04:39:30 more
  • WordPress程式不能升級為5.4.2版本的原因

    WordPress是一款個人博客系統,受到英文博客愛好者和中文博客愛好者的追捧,并逐步演化成一款內容管理系統軟體;它是使用PHP語言和MySQL資料庫開發的,用戶可以在支持PHP和MySQL資料庫的服務器上使用自己的博客。每一次WordPress程式的更新,就會牽動無數WordPress愛好者的心, ......

    uj5u.com 2020-09-10 04:39:49 more
  • 使用CSS3的偽元素進行首字母下沉和首行改變樣式

    網頁中常見的一種效果,首字改變樣式或者首行改變樣式,效果如下圖。 代碼: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, ......

    uj5u.com 2020-09-10 04:40:09 more
  • 關于a標簽的講解

    什么是a標簽? <a> 標簽定義超鏈接,用于從一個頁面鏈接到另一個頁面。 <a> 元素最重要的屬性是 href 屬性,它指定鏈接的目標。 a標簽的語法格式:<a href=https://www.cnblogs.com/summerxbc/p/"指定要跳轉的目標界面的鏈接">需要展示給用戶看見的內容</a> a標簽 在所有瀏覽器中,鏈接的默認外觀如下: 未被訪問的鏈接帶 ......

    uj5u.com 2020-09-10 04:40:11 more
  • 前端輪播圖

    在需要輪播的頁面是引入swiper.min.js和swiper.min.css swiper.min.js地址: 鏈接:https://pan.baidu.com/s/15Uh516YHa4CV3X-RyjEIWw 提取碼:4aks swiper.min.css地址 鏈接:https://pan.b ......

    uj5u.com 2020-09-10 04:40:13 more
  • 如何設定html中的背景圖片(全屏顯示,且不拉伸)

    1 <style>2 body{background-image:url(https://uploadbeta.com/api/pictures/random/?key=BingEverydayWallpaperPicture); 3 background-size:cover;background ......

    uj5u.com 2020-09-10 04:40:16 more
  • Java學習——HTML詳解(上)

    HTML詳解 初識HTML Hyper Text Markup Language(超文本標記語言) 1 <!--DOCTYPE:告訴瀏覽器我們要使用什么規范--> 2 <!DOCTYPE html> 3 <html lang="en"> 4 <head> 5 <!--meta 描述性的標簽,描述一些 ......

    uj5u.com 2020-09-10 04:40:33 more
最新发布
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 07:59:23 more
  • 生產事故-走近科學之消失的JWT

    入職多年,面對生產環境,盡管都是小心翼翼,慎之又慎,還是難免捅出簍子。輕則滿頭大汗,面紅耳赤。重則系統停擺,損失資金。每一個生產事故的背后,都是寶貴的經驗和教訓,都是專案成員的血淚史。為了更好地防范和遏制今后的各類事故,特開此專題,長期更新和記錄大大小小的各類事故。有些是親身經歷,有些是經人耳傳口授 ......

    uj5u.com 2023-04-18 07:55:04 more
  • 記錄--Canvas實作打飛字游戲

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 打開游戲界面,看到一個畫面簡潔、卻又富有挑戰性的游戲。螢屏上,有一個白色的矩形框,里面不斷下落著各種單詞,而我需要迅速地輸入這些單詞。如果我輸入的單詞與螢屏上的單詞匹配,那么我就可以獲得得分;如果我輸入的單詞錯誤或者時間過長,那么我就會輸 ......

    uj5u.com 2023-04-04 08:35:30 more
  • 了解 HTTP 看這一篇就夠

    在學習網路之前,了解它的歷史能夠幫助我們明白為何它會發展為如今這個樣子,引發探究網路的興趣。下面的這張圖片就展示了“互聯網”誕生至今的發展歷程。 ......

    uj5u.com 2023-03-16 11:00:15 more
  • 藍牙-低功耗中心設備

    //11.開啟藍牙配接器 openBluetoothAdapter //21.開始搜索藍牙設備 startBluetoothDevicesDiscovery //31.開啟監聽搜索藍牙設備 onBluetoothDeviceFound //30.停止監聽搜索藍牙設備 offBluetoothDevi ......

    uj5u.com 2023-03-15 09:06:45 more
  • canvas畫板(滑鼠和觸摸)

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>canves</title> <style> #canvas { cursor:url(../images/pen.png),crosshair; } #canvasdiv{ bo ......

    uj5u.com 2023-02-15 08:56:31 more
  • 手機端H5 實作自定義拍照界面

    手機端 H5 實作自定義拍照界面也可以使用 MediaDevices API 和 <video> 標簽來實作,和在桌面端做法基本一致。 首先,使用 MediaDevices.getUserMedia() 方法獲取攝像頭媒體流,并將其傳遞給 <video> 標簽進行渲染。 接著,使用 HTML 的 < ......

    uj5u.com 2023-01-12 07:58:22 more
  • 記錄--短視頻滑動播放在 H5 下的實作

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 短視頻已經無數不在了,但是主體還是使用 app 來承載的。本文講述 H5 如何實作 app 的視頻滑動體驗。 無聲勝有聲,一圖頂百辯,且看下圖: 網址鏈接(需在微信或者手Q中瀏覽) 從上圖可以看到,我們主要實作的功能也是本文要講解的有: ......

    uj5u.com 2023-01-04 07:29:05 more
  • 一文讀懂 HTTP/1 HTTP/2 HTTP/3

    從 1989 年萬維網(www)誕生,HTTP(HyperText Transfer Protocol)經歷了眾多版本迭代,WebSocket 也在期間萌芽。1991 年 HTTP0.9 被發明。1996 年出現了 HTTP1.0。2015 年 HTTP2 正式發布。2020 年 HTTP3 或能正... ......

    uj5u.com 2022-12-24 06:56:02 more
  • 【HTML基礎篇002】HTML之form表單超詳解

    ??一、form表單是什么

    ??二、form表單的屬性

    ??三、input中的各種Type屬性值

    ??四、標簽 ......

    uj5u.com 2022-12-18 07:17:06 more