Skip to main content
如何用JavaScript实现贪吃蛇游戏

如何用JavaScript实现贪吃蛇游戏

· 0 · 0 ·
javascript
小绵尾巴
Author
小绵尾巴
正在路上的前端工程师
Table of Contents

我们都玩过贪吃蛇游戏,在写出一个贪吃蛇游戏前,先复习一下游戏的玩法:

  1. 贪吃蛇向食物格移动,用方向键控制贪吃蛇的移动方向。
  2. 每吃下一个食物格,贪吃蛇的身体会增长一格。
  3. 当贪吃蛇碰到游戏四周的边界线,或者咬到自己的身体,则游戏结束。

Prerequisite #

这篇博文假设你已经学过JavaScript中的:

  • Array
  • Object
  • HTML Canvas
  • DOM
  • addEventListener()
  • UI Events
  • setTimeout()

HTML #

写一个JavaScript程序一定会从HTML开始,写出页面的基本骨架,这里会用到HTML5的<canvas>标签:

<div class="container">
    <canvas id="gameBoard" width="500" height="500"></canvas>
    <div id="scoreDisplay">0</div>
    <button id="restartBtn">Play Again</button>
</div> 
  • gameBoard<canvas>来定义一个宽500px,高500px的画布,用JavaScript在画布上绘制贪吃蛇与食物。
  • scoreDisplay<div>来显示当前获得的游戏分数。
  • restartBtn按钮<button>重新开始游戏。

CSS #

#gameBorad{
    border: 3px solid;
}

只有「添加画布的边框」这件事是最重要的,因为玩家需要看到贪吃蛇不能碰到的“四个墙壁”,其他元素的样式可加可不加。

JavaScript #

第一件事,声明需要的变量。

  1. 选中JavaScript中的元素:

    const gameBoard = document.querySelector("#gameBoard");
    const context = gameBoard.getContext("2d");
    
    const scoreDisplay = document.querySelector("#scoreDisplay");
    const restartBtn = document.querySelector("#restartBtn");
    
    • 选中<canvas>元素,创建context对象。getContext("2d")对象是内建的 HTML5 对象,可以绘制各种图形。
    • 选中分数显示scoreDisplay和重启按钮restartBtn
  2. 声明游戏画布的宽度与高度:

    const gameWidth = gameBoard.width;
    const gameHeight = gameBoard.height;
    
  3. 声明颜色,在画布上绘制图形时使用:

    const boardBackground = "#ffffff";
    const snakeColor = "#53b096";
    const snakeBorder = "#000000";
    const foodColor = "#d44444";
    
  4. 声明其他变量:

    const unitSize = 25;
    
    let running = false;
    
    let score = 0;
    
    • unitSize是游戏中的单位,不管是一个食物格的大小,还是贪吃蛇的一个身体部位的大小,包括贪吃蛇移动的速度,都会以unitSize为单位,这里设置为25px。
    • running表示游戏是否正在运行的状态。
    • score表示游戏的分数。
  5. 声明移动速度:

    let xVelocity = unitSize; 
    let yVelocity = 0;
    
    • xVelocity表示每个游戏单位时间x轴移动的速度。
      • 如果xVelocity是正数,贪吃蛇向右移动;如果是负数,贪吃蛇向左移动。
      • 一开始设置为一个unitSize,表示向右移动一个unitSize单位。
    • yVelocity表示每个游戏单位时间y轴移动的速度。
      • 如果yVelocity是正数,贪吃蛇向下移动;如果是负数,贪吃蛇向上移动。
      • 一开始设置为0,表示开始时即不向上移动也不向下移动。
  6. 声明食物格的坐标变量:

    let foodX;
    let foodY;
    
    • foodX是食物格在画布中的x坐标
    • foodY是食物格在画布中的y坐标
    • 食物格的坐标会用函数createFood()随机生成。
  7. 声明一个对象数组表示贪吃蛇:

    let snake = [
            {x:unisize * 4, y:0},
            {x:unisize * 3, y:0},
            {x:unisize * 2, y:0},
            {x:unisize, y:0},
            {x:0 y:0},
    ]
    
    • 定义初始贪吃蛇的长度是5格。
    • 每个对象中的属性是贪吃蛇身体每个部分在画布中的x坐标y坐标

声明完变量后,我们给windows窗口添加addEventListner监听键盘事件,监听←↓↑→方向键是否被按下。如果监听到方向键被按下,则执行changeDirection()函数改变贪吃蛇移动的方向,这个函数会在稍后被定义。

window.addEventListener("keydown", changeDirection);

restartBtn按钮添加addEventListener监听鼠标事件,当按钮被点击时,执行restartGame()函数,在稍后也会被定义。

restartBtn.addEventListener("click", restartGame);

调用开始游戏的函数startGame(),稍后定义。

startGame();

解下来定义我们所有需要的函数。

The startGame function #

function startGame(){
    running = true;
    scoreDisplay.textContent = score;
    createFood();
    drawFood();
    nextTick();
}
  • 设置运行状态runningtrue,表示游戏开始。
  • 将页面上的游戏分数更改为当前获得的游戏分数。
  • 依次调用createFood()drawFood()nextTick()函数。

The nextTick function #

nextTick()是每个游戏时间单位都会做的事情。

function nextTick(){
   if(running){
       setTimeout(() =>{
           clearBoard();
           drawFood();
           moveSnake();
           drawSnake(); 
           checkGameOver(); 
           nextTick(); 
       }, 75)
   }
   else{
       displayGameOver();
   }
}
  • 如果游戏正在进行,设置75毫秒后会做的事情:清除画布,绘制食物格,移动贪吃蛇,绘制贪吃蛇,检查游戏是否结束,再调用一次nextTick()函数,这样就能不断重复这些过程,并在游戏结束时停下。
  • 如果游戏停止,则在页面上提醒游戏结束。

The clearBoard function #

clearBoard()函数用于重画画布。

function clearBoard(){
    context.fillStyle = boardBackground;
    context.fillRect(0, 0, gameWidth, gameHeight);
}
  • fillStyle设置画布的填充颜色。
  • fillRect(x,y,width,height)绘制一个画布,从坐标(0,0)开始,宽度和高度都是画布的宽度和高度。

The creatFood function #

createFood会随机在画布中找到一个位置放置食物格。

function createFood(){
    function randomFood(min, max){
        const randNum = Math.round((Math.random() * (max - min) + min) / unitSize) * unitSize;
        return randNum;
    }
    foodX = randomFood(0, gameWidth - unitSize);
    foodY = randomFood(0, gameWidth - unitSize);
} 
  • 定义一个内嵌函数randomFood生成能被unitSize整除的随机数。
    • (Math.random() * (max - min) + min)会生成在minmax之间的随机数
    • 这个Math.round()取得这个随机数除以unitSize的整数,得到这个随机数一共有多少个UnitSize
    • 此时再乘以UnitSize,就会得到范围在min~max之间,并且无论如何都会被unitSize整除的随机数。
  • 食物格x轴的范围是0gameWidth - unitSize,y轴也一样。
  • 分别随机生成食物格x轴的值与y轴的值,得到食物格的位置。

The drawFood function #

drawFood会在游戏画布中绘制出食物格。

function drawFood(){
    context.fillStyle = foodColor;
    context.fillRect(foodX, foodY, unitSize, unitSize);
}
  • fillStyle设置图形的填充颜色。
  • fillRect(x,y,width,height)绘制一个方形,食物格的x轴与y轴,在createFood()函数中已经随机生成,食物格的宽度和高度都会是一个unitSize

The moveSnake function #

移动贪吃蛇的思路是:在贪吃蛇的移动方向创建一个贪新的头部方块,并消除尾巴方块,这样看起来就像在移动一样。

function moveSnake(){
    const head = {x: snake[0].x + xVelocity,
                  y: snake[0].y + yVelocity};
    snake.unshift(head);
    if(snake[0].x == foodX && snake[0].y == foodY){
        score++;
        scoreDisplay.textContent = score;
        createFood();
    }
    else{
        snake.pop();
    }
}
  • 创建一个新的头部方块并用Array.unshift()方法向数组的开头添加该头部。
  • 判断贪吃蛇是否吃掉了食物格,如果贪吃蛇头部的坐标和食物格的坐标重合,那么就是吃掉了食物格。此时将游戏分数加1,并再创建一个食物。
  • 如果没有吃掉食物格,那么贪吃蛇在移动,用Array.pop()会删除数组的最后一个元素,在这里,就是删除贪吃蛇的尾巴方块。

The drawSnake function #

function drawSnake(){
    context.fillStyle = snakeColor;
    context.strokeStyle = snakeBorder;
    snake.forEach(snakePart => {
        context.fillRect(snakePart.x, snakePart.y, unitSize, unitSize);
        context.strokeRect(snakePart.x, snakePart.y, unitSize, unitSize);
    })
}
  • 因为snake是一个数组对象,用forEach遍历贪吃蛇的每一个身体部位,并画出方块与边框。

The changeDirection function #

function changeDirection(event){
    const keyPressed = event.keyCode;
    const LEFT = 37;
    const UP = 38;
    const RIGHT = 39;
    const DOWN = 40;

    const goingUp = (yVelocity == -unitSize);
    const goingDown = (yVelocity == unitSize);
    const goingRight = (xVelocity == unitSize);
    const goingLeft  = (xVelocity == -unitSize);

    switch(true){
        case (keyPressed == LEFT && !goingRight): 
            xVelocity = -unitSize; 
            yVelocity = 0;
            break;
        case (keyPressed == UP && !goingDown):
            xVelocity = 0;
            yVelocity = -unitSize;
            break;
        case (keyPressed == RIGHT && !goingLeft):
            xVelocity = unitSize;
            yVelocity = 0;
            break;
        case (keyPressed == DOWN && !goinUp):
            xVelocity = 0;
            yVelocity =  unitSize;
            break;
    }
}
  • keyCode表示键盘上的按键键的键码值,keyPressed存储当前按下的按键的键码值。方向键的键码值分别是:
    • ←: 37
    • ↑: 38
    • →: 39
    • ↓: 40
  • 用描述性的语言LEFTUPRIGHTDOWN存储这些键码值。
  • goingUpgoingDowngoingRightgoingLeft返回的是布尔值。
  • 判断keyPressed == LEFT && !goingRight的目的是保证按下左方向键←后,可以继续向左向上或向下,当不能向右,因为向右将咬到自己输掉游戏。

The checkGameOver function #

游戏结束有两种情况,一种情况是,贪吃蛇碰到游戏的边框,第二种情况是贪吃蛇咬到自己。

function checkGameOver(){
    switch(true){
        case (snake[0].x < 0):
            running = false;
            break;
        case (snake[0].x >= gameWidth):
            running = false;
            break;
        case (snake[0].y < 0):
            running = false;
            break;
        case (snake[0].y >= gameHeight):
            running = false;
            break;
    }
    for(let i = 1; i < snake.length; i++){
        if(snake[i].x == snake[0].x && snake[i].y == snake[0].y){
            running = false; 
        }
    }
}
  • 用switch条件语句判断是否碰到游戏画布的边框:
    • 当贪吃蛇头部的x坐标小于0,说明贪吃蛇碰到了左边的边框,结束游戏。
    • 当贪吃蛇头部的x坐标大于画布的宽度,说明贪吃蛇碰到了右边的边框,结束游戏。
    • 当贪吃蛇头部的y坐标小于0,说明贪吃蛇碰到了上面的边框,结束游戏。
    • 当贪吃蛇头部的y坐标大于画布的高度,说明贪吃蛇碰到了下面的边框,结束游戏。
  • 用for循环遍历贪吃蛇身体的每个部分,判断贪吃蛇的头部是否与身体的任何一个部分重合。

The display GameOver function #

在游戏的中间显示“GAME OVER!”

function displayGameOver(){
    context.font = "50px Shantell Sans";
    context.fillStyle = "black";
    context.textAlign = "center";
    context.fillText("GAME OVER!", gameWidth / 2, gameHeight /2);
    running = false;
}

The restartGame function #

function restartGame(){
    score = 0;
    xVelocity = unitSize;
    yVelocity = 0;
    snake = [
        {x:unitSize * 4, y:0},
        {x:unitSize * 3, y:0},
        {x:unitSize * 2, y:0},
        {x:unitSize, y:0},
        {x:0, y:0},
    ];
    startGame();
}
  • 将游戏分数和移动速度重置为0。
  • 重新创建一个snake
  • 调用开始游戏startGame()函数。

到此为止,已经成功写出基本的贪吃蛇游戏了。

结语 #

发现贪吃蛇能很好的练习HTML5的Canvas,和JavaScript的DOM与事件,值得反复练习。

Reference #




评论