怎樣用JavaScript實現一個簡單的貪吃蛇游戲?

游戲的核心循環通過setinterval驅動,分為更新和繪制兩個階段。1. 更新階段處理蛇的移動、碰撞檢測和食物邏輯;2. 繪制階段將最新狀態渲染到canvas上。蛇的移動通過計算新頭部位置并更新數組實現,使用unshift添加頭部和pop移除尾部模擬移動效果。碰撞檢測包含三種情況:撞墻(超出畫布邊界)、撞自己(頭部與身體坐標重合)和撞食物(得分并增長蛇身)。生成食物時通過隨機坐標并檢查是否與蛇身重疊,若重疊則遞歸重新生成,確保食物出現在空閑位置。

怎樣用JavaScript實現一個簡單的貪吃蛇游戲?

JavaScript實現一個簡單的貪吃蛇游戲,核心在于構建一個游戲循環,在這個循環里不斷更新蛇的位置、繪制游戲狀態,并響應玩家的鍵盤輸入。它是一個非常經典的入門項目,能很好地幫助你理解游戲開發中的基本概念,比如游戲循環、碰撞檢測和狀態管理。

怎樣用JavaScript實現一個簡單的貪吃蛇游戲?

解決方案

怎樣用JavaScript實現一個簡單的貪吃蛇游戲?

要構建一個貪吃蛇游戲,我們需要html來提供一個畫布(canvas),css來簡單美化一下,然后用JavaScript來處理所有的游戲邏輯。

立即學習Java免費學習筆記(深入)”;

首先,在HTML中創建一個canvas元素:

怎樣用JavaScript實現一個簡單的貪吃蛇游戲?

<!DOCTYPE html> <html lang="zh-CN"> <head>     <meta charset="UTF-8">     <meta name="viewport" content="width=device-width, initial-scale=1.0">     <title>簡單的貪吃蛇</title>     <style>         body { margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background-color: #222; }         canvas { background-color: #000; border: 2px solid #555; display: block; }     </style> </head> <body>     <canvas id="gameCanvas" width="400" height="400"></canvas>     <script src="snake.JS"></script> </body> </html>

接著,是snake.js的核心邏輯:

const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d');  const gridSize = 20; // 每個方塊的大小 const tileCount = canvas.width / gridSize; // 一行/一列有多少個方塊  let snake = [{ x: 10, y: 10 }]; // 蛇的初始位置 let food = {}; // 食物的位置 let dx = 0; // x方向的速度 let dy = 0; // y方向的速度 let score = 0; let changingDirection = false; // 防止快速按鍵導致方向沖突  // 游戲主循環,我個人偏愛用 setInterval,在簡單的游戲中它足夠直觀 let gameInterval;  function generateFood() {     food = {         x: math.floor(Math.random() * tileCount),         y: Math.floor(Math.random() * tileCount)     };      // 確保食物不生成在蛇身上     for (let i = 0; i < snake.length; i++) {         if (food.x === snake[i].x && food.y === snake[i].y) {             generateFood(); // 遞歸調用直到找到一個空位             return;         }     } }  function draw() {     // 清空畫布     ctx.clearRect(0, 0, canvas.width, canvas.height);      // 繪制食物     ctx.fillStyle = 'red';     ctx.strokeStyle = 'darkred';     ctx.fillRect(food.x * gridSize, food.y * gridSize, gridSize, gridSize);     ctx.strokeRect(food.x * gridSize, food.y * gridSize, gridSize, gridSize);      // 繪制蛇     ctx.fillStyle = 'lime';     ctx.strokeStyle = 'darkgreen';     snake.forEach(segment => {         ctx.fillRect(segment.x * gridSize, segment.y * gridSize, gridSize, gridSize);         ctx.strokeRect(segment.x * gridSize, segment.y * gridSize, gridSize, gridSize);     }); }  function update() {     changingDirection = false; // 允許再次改變方向      const head = { x: snake[0].x + dx, y: snake[0].y + dy };      // 碰撞檢測     if (head.x < 0 || head.x >= tileCount ||         head.y < 0 || head.y >= tileCount ||         checkCollision(head)) {         clearInterval(gameInterval); // 游戲結束         alert(`游戲結束!得分:${score}`);         return;     }      snake.unshift(head); // 將新頭部添加到蛇的數組開頭      const didEatFood = head.x === food.x && head.y === food.y;     if (didEatFood) {         score += 10;         generateFood(); // 生成新食物     } else {         snake.pop(); // 如果沒吃到食物,移除尾巴     } }  function checkCollision(head) {     // 檢查頭部是否與身體其他部分碰撞     for (let i = 1; i < snake.length; i++) {         if (head.x === snake[i].x && head.y === snake[i].y) {             return true;         }     }     return false; }  function changeDirection(event) {     if (changingDirection) return;     changingDirection = true;      const keyPressed = event.keyCode;     const LEFT_KEY = 37;     const RIGHT_KEY = 39;     const UP_KEY = 38;     const DOWN_KEY = 40;      const goingUp = dy === -1;     const goingDown = dy === 1;     const goingLeft = dx === -1;     const goingRight = dx === 1;      // 避免蛇立即掉頭     if (keyPressed === LEFT_KEY && !goingRight) {         dx = -1;         dy = 0;     }     if (keyPressed === UP_KEY && !goingDown) {         dx = 0;         dy = -1;     }     if (keyPressed === RIGHT_KEY && !goingLeft) {         dx = 1;         dy = 0;     }     if (keyPressed === DOWN_KEY && !goingUp) {         dx = 0;         dy = 1;     } }  // 初始化游戲 function startGame() {     generateFood();     document.addEventListener('keydown', changeDirection);     // 初始方向,讓蛇開始移動     dx = 1;     dy = 0;     gameInterval = setInterval(() => {         update();         draw();     }, 100); // 100毫秒更新一次,可以調整速度 }  startGame();

這個方案涵蓋了游戲的基本要素:畫布設置、蛇和食物的表示、繪制函數、更新游戲狀態的函數以及鍵盤事件監聽。它是一個相當基礎但完整的實現。

游戲的核心循環是如何運作的?

在我看來,游戲的核心循環就像是游戲的心臟,它跳動著,驅動著整個世界的運轉。在貪吃蛇這種簡單的2D游戲中,這個“跳動”通常通過一個定時器來實現。你可能會看到兩種主要的方式:setInterval 和 requestAnimationFrame。

對于貪吃蛇這種基于網格、狀態更新相對離散的游戲,我個人更傾向于使用setInterval。它簡單直觀,你設置一個固定的時間間隔(比如100毫秒),然后告訴瀏覽器每隔這么久就執行一次我的游戲邏輯。它的好處是,你對游戲的速度有非常精確的控制,每100毫秒蛇就移動一步,不會因為幀率波動導致蛇忽快忽慢。

這個循環里通常會包含兩個主要階段:

  1. 更新(Update):這個階段是純粹的邏輯處理。蛇的位置變了沒?吃到食物了沒?撞墻了沒?這些都在這里計算。它不涉及任何的視覺呈現,只是修改游戲內部的數據狀態。比如,蛇的頭部坐標會根據當前的方向進行調整,如果吃了食物,蛇的身體數組會增長,否則就移除尾部。
  2. 繪制(Draw):在更新完所有游戲狀態后,就需要把這些新的狀態“畫”到屏幕上。清除舊的畫面,然后根據最新的蛇和食物坐標,在畫布上重新繪制它們。

所以,整個流程就是:設定一個時間間隔 -> 在每個間隔里,先更新所有游戲數據 -> 然后根據新數據重新繪制畫面 -> 重復。這個循環不斷進行,直到游戲結束。雖然requestAnimationFrame在動畫平滑度和資源優化上更有優勢,因為它會與瀏覽器繪制周期同步,但對于這種步進式的游戲,setInterval的固定步長反而讓邏輯更清晰。當然,如果未來想做更復雜的動畫效果,比如蛇的平滑過渡,那requestAnimationFrame就是更好的選擇。

如何處理蛇的移動和碰撞檢測?

蛇的移動和碰撞檢測,是貪吃蛇游戲里最核心也最容易出錯的部分。我通常會把它們看作一個緊密相連的舞蹈:蛇每一步的移動都伴隨著對周圍環境的“審視”,看有沒有撞到什么。

蛇的移動:

蛇的移動,從邏輯上講,并不是讓整個蛇身一起平移。它更像是一個“頭部先行,身體跟隨”的過程。

  1. 確定新頭部位置: 根據當前的方向(上、下、左、右),計算出蛇頭即將到達的新坐標。比如,如果當前是向右移動(dx = 1, dy = 0),那么新頭部x坐標就是當前頭部x坐標加1。
  2. 添加新頭部: 將這個新計算出的頭部坐標添加到蛇身體數組的最前面?,F在,蛇的數組長度暫時增加了1。
  3. 移除舊尾部(如果沒吃到食物): 如果蛇頭沒有碰到食物,這意味著蛇只是單純地向前移動了一格,所以需要從蛇身體數組的末尾移除最后一個元素,從而保持蛇的長度不變。如果蛇頭碰到了食物,那么就移除尾部,這樣蛇的身體長度就自然增加了一格,模擬了“吃”和“長大”的效果。

這種處理方式非常優雅,避免了復雜的循環來移動每個蛇節,而是通過數組的unshift和pop方法巧妙地實現了蛇的移動和增長。

碰撞檢測:

碰撞檢測是游戲邏輯中判斷“發生了什么”的關鍵。在貪吃蛇中,主要有三種碰撞需要處理:

  1. 撞墻: 這是最直接的。只需要檢查新計算出的蛇頭坐標是否超出了畫布的邊界。例如,如果蛇頭的x坐標小于0(超出左邊界),或者大于等于tileCount(超出右邊界),那就說明撞墻了。y坐標同理。一旦撞墻,游戲就應該結束。
  2. 撞自己: 這是比較有趣的一種。蛇頭不能碰到自己身體的任何一部分。實現方法是,在計算出新蛇頭的位置后,遍歷蛇身體數組(從第二個元素開始,因為第一個元素就是當前蛇頭,自己不會撞自己),檢查新蛇頭的坐標是否與任何一個身體節的坐標重合。如果重合了,同樣游戲結束。這里有個小細節,如果蛇剛開始只有一兩個節,是不會發生自撞的,所以循環從i = 1開始很關鍵。
  3. 撞食物: 這是玩家希望發生的碰撞!同樣,檢查新蛇頭的坐標是否與食物的坐標重合。如果重合了,那么蛇就“吃”到了食物。這時候,除了增加分數,更重要的是要執行“蛇增長”的邏輯(即上面提到的,不移除蛇尾),并且在畫布上生成一個新的食物。

這些碰撞檢測都需要在蛇的頭部移動之后繪制之前進行,這樣才能在視覺上及時反映出游戲狀態的變化,并決定游戲是否繼續。

如何生成隨機食物并確保其不出現在蛇身上?

生成隨機食物聽起來簡單,但要確保它不出現在蛇身上,就多了一層考量。我通常會采用一個“先生成,再檢查,不合格就重來”的策略。

  1. 隨機位置生成: 首先,利用 Math.random() 和 Math.floor() 在游戲網格的范圍內生成一對隨機的 (x, y) 坐標。因為我們的游戲是基于網格的,所以生成的坐標應該是0到tileCount – 1之間的整數。

    food = {     x: Math.floor(Math.random() * tileCount),     y: Math.floor(Math.random() * tileCount) };

    這確保了食物會落在畫布內的某個網格單元上。

  2. 檢查與蛇身重疊: 生成一個潛在的食物位置后,我們需要檢查這個位置是否已經被蛇占據了。我會遍歷蛇的每一個身體節(包括蛇頭),比較食物的 (x, y) 坐標是否與任何一個蛇節的 (x, y) 坐標相同。

    for (let i = 0; i < snake.length; i++) {     if (food.x === snake[i].x && food.y === snake[i].y) {         // 重疊了!     } }
  3. 不合格則重新生成: 如果發現新生成的食物位置與蛇身重疊,那么這個位置就是無效的。這時候,最直接的辦法就是重新調用生成食物的函數。這形成了一個遞歸或者循環,直到找到一個完全空閑的位置為止。

    function generateFood() {     food = {         x: Math.floor(Math.random() * tileCount),         y: Math.floor(Math.random() * tileCount)     };      // 確保食物不生成在蛇身上     for (let i = 0; i < snake.length; i++) {         if (food.x === snake[i].x && food.y === snake[i].y) {             generateFood(); // 遞歸調用直到找到一個空位             return; // 找到重疊后,本次生成無效,直接返回等待下一次遞歸         }     }     // 如果循環結束都沒有重疊,說明這個位置是合格的 }

    這種遞歸方式在蛇比較短的時候效率很高。但理論上,如果蛇非常長,幾乎占據了整個屏幕,那么找到一個空閑位置可能會需要多次嘗試,甚至在極端情況下(比如屏幕全被蛇占滿)會陷入無限循環。不過對于簡單的貪吃蛇游戲,這種情況通常不會發生,所以這種簡單直接的遞歸方法是完全可行的。

通過這種“生成-檢查-重試”的模式,我們就能確保食物總是出現在一個對玩家來說可達且有效的空閑位置上。

? 版權聲明
THE END
喜歡就支持一下吧
點贊6 分享