JavaScript Snake 教程解释

使用HTML、CSS和JavaScript构建贪吃蛇游戏

在这篇文章中,我将详细介绍如何利用HTML、CSS和JavaScript来开发一款经典的贪吃蛇游戏。我们不会依赖任何外部库,所有代码都将在浏览器环境中运行。这个项目不仅是一项有趣的技术实践,也是锻炼你编程思维和解决问题能力的绝佳机会。

贪吃蛇游戏的核心玩法很简单:控制一条蛇在游戏区域内移动,避开障碍物,并尽可能多地吞噬食物。每当蛇成功吃到食物时,它的身体就会增长一段长度。随着游戏进行,蛇的身体越来越长,操控难度也会随之增加。

玩家需要注意,蛇不能撞到游戏边界或者自身的身体。所以,随着蛇的长度增加,游戏挑战性也随之提高。

本教程的目标是指导你完成一个完整的贪吃蛇游戏构建过程,让你深入理解其中的原理。

你可以从我的 GitHub 仓库找到完整的源代码。游戏的演示版本托管在 GitHub Pages

准备工作

这个项目将使用HTML、CSS和JavaScript进行构建。虽然我们会使用基本的HTML和CSS进行页面布局和样式设置,但JavaScript才是我们关注的重点。因此,你应当对JavaScript有一定的了解。如果你还不熟悉JavaScript,我建议你先学习一些相关的基础知识。当然,你还需要一个代码编辑器和一个浏览器来编写和测试代码,如果你正在阅读这篇文章,你很可能已经拥有了这两样工具。

项目设置

首先,我们需要创建项目文件。在一个空文件夹中,创建一个名为`index.html`的文件,并加入以下HTML标记:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="https://wilku.top/javascript-snake-tutorial-explained/./styles.css" />
    <title>Snake</title>
  </head>
  <body>
    <div id="game-over-screen">
      <h1>Game Over</h1>
    </div>
    <canvas id="canvas" width="420" height="420"> </canvas>
    <script src="./snake.js"></script>
  </body>
</html>

这段代码创建了一个基础的“游戏结束”界面,我们将通过JavaScript来控制它的显示与隐藏。它还定义了一个画布(canvas)元素,我们将在其上绘制游戏地图、蛇和食物。此外,这段HTML代码还引入了样式表(styles.css)和JavaScript代码文件(snake.js)。

接着,创建一个名为`styles.css`的文件,并添加以下CSS样式:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'Courier New', Courier, monospace;
}

body {
    height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    background-color: #00FFFF;
}

#game-over-screen {
    background-color: #FF00FF;
    width: 500px;
    height: 200px;
    border: 5px solid black;
    position: absolute;
    align-items: center;
    justify-content: center;
    display: none;
}

这段CSS代码首先重置了所有元素的内外边距,并将盒模型设置为`border-box`,同时设置了默认字体。对于`body`元素,我们将其高度设置为视口的100%,并使用Flexbox将内容居中显示。我们还为背景设置了青色。最后,我们为“游戏结束”屏幕设置了样式,包括尺寸、边框、背景颜色以及绝对定位,确保其在页面中心显示。默认情况下,我们将`display`属性设置为`none`,使其初始状态是隐藏的。

最后,我们需要创建一个`snake.js`文件,我们将在接下来的章节中编写这个文件的内容。

定义全局变量

在`snake.js`文件中,首先声明一些全局变量。这些变量将在整个游戏中被使用,所以将它们放在文件的顶部:

// 获取HTML元素的引用
let gameOverScreen = document.getElementById("game-over-screen");
let canvas = document.getElementById("canvas");

// 获取画布的绘图上下文
let ctx = canvas.getContext("2d");

这里我们获取了对“游戏结束”屏幕和画布元素的引用,并且创建了一个绘图上下文,这个上下文将用于在画布上进行绘制操作。

接着,添加以下变量定义,用于设置游戏网格和单位长度:

// 定义游戏网格
let gridSize = 400;
let unitLength = 10;

`gridSize`定义了游戏网格的大小(以像素为单位),`unitLength`定义了游戏中的基本单位长度,这个单位长度将用于定义蛇、食物和游戏边界的尺寸和移动步长。

最后,我们还需要一些变量来追踪游戏状态:

// 定义游戏状态变量
let snake = [];
let foodPosition = { x: 0, y: 0 };
let direction = "right";
let collided = false;

`snake`数组用于存储蛇的身体位置,每个元素代表蛇的一个身体部分,包含x和y坐标。`foodPosition`变量保存食物的当前位置。`direction`变量记录蛇的移动方向,`collided`是一个布尔值,用于标记是否发生碰撞。

声明函数

为了更好地组织代码,我们将游戏逻辑分解为多个函数。以下是我们需要声明的函数及其功能:

function setUp() {}
function doesSnakeOccupyPosition(x, y) {}
function checkForCollision() {}
function generateFood() {}
function move() {}
function turn(newDirection) {}
function onKeyDown(e) {}
function gameLoop() {}

`setUp`函数负责初始化游戏,`checkForCollision`检查蛇是否发生碰撞,`doesSnakeOccupyPosition`检查某个位置是否被蛇占据,`generateFood`生成食物,`move`控制蛇的移动,`turn`改变蛇的移动方向,`onKeyDown`监听键盘事件,`gameLoop`控制游戏的主要循环。

定义函数

现在我们开始定义这些函数。每个函数都将有详细的解释和注释,确保你理解其背后的逻辑。

`setUp` 函数

`setUp`函数的主要任务是设置游戏初始状态,包括绘制游戏边界、初始化蛇的位置以及生成初始食物位置:

  // 在画布上绘制边界
  // 画布的大小是网格大小加上两侧边框的厚度
  canvasSideLength = gridSize + unitLength * 2;

  // 绘制一个覆盖整个画布的黑色矩形
  ctx.fillRect(0, 0, canvasSideLength, canvasSideLength);

  // 清除黑色矩形的中心区域,形成游戏区域,留下黑色边框
  ctx.clearRect(unitLength, unitLength, gridSize, gridSize);

  // 接下来,初始化蛇的头部和尾部位置
  // 蛇的初始长度为6个单位长度或60px

  // 蛇头的位置在网格中心前方3个单位
  const headPosition = Math.floor(gridSize / 2) + 30;

  // 蛇尾的位置在网格中心后方3个单位
  const tailPosition = Math.floor(gridSize / 2) - 30;

  // 从尾部到头部,以单位长度为步长循环
  for (let i = tailPosition; i <= headPosition; i += unitLength) {

    // 存储蛇身体的位置,并在画布上绘制
    snake.push({ x: i, y: Math.floor(gridSize / 2) });

    // 在该位置绘制一个单位长度的矩形
    ctx.fillRect(x, y, unitLength, unitLength);
  }

  // 生成食物
  generateFood();

`doesSnakeOccupyPosition` 函数

此函数接受x和y坐标作为参数,检查蛇的身体是否占据了该位置。它使用数组的find方法来检查蛇的身体中是否有位置与传入的坐标匹配:

function doesSnakeOccupyPosition(x, y) {
  return !!snake.find((position) => {
    return position.x == x && y == foodPosition.y;
  });
}

`checkForCollision` 函数

此函数负责检查蛇是否与游戏边界或者自身发生碰撞。如果发生碰撞,它会将`collided`变量设置为`true`:

 function checkForCollision() {
  const headPosition = snake.slice(-1)[0];
  // 检查与左右边界的碰撞
  if (headPosition.x < 0 || headPosition.x >= gridSize - 1) {
    collided = true;
  }

  // 检查与上下边界的碰撞
  if (headPosition.y < 0 || headPosition.y >= gridSize - 1) {
    collided = true;
  }

  // 检查与自身身体的碰撞
  const body = snake.slice(0, -2);
  if (
    body.find(
      (position) => position.x == headPosition.x && position.y == headPosition.y
    )
  ) {
    collided = true;
  }
}

`generateFood` 函数

此函数使用do-while循环来生成食物的位置,并确保食物的位置不与蛇的身体重叠。一旦找到合适的位置,食物会被绘制在画布上:

function generateFood() {
  let x = 0,
    y = 0;
  do {
    x = Math.floor((Math.random() * gridSize) / 10) * 10;
    y = Math.floor((Math.random() * gridSize) / 10) * 10;
  } while (doesSnakeOccupyPosition(x, y));

  foodPosition = { x, y };
  ctx.fillRect(x, y, unitLength, unitLength);
}

`move` 函数

此函数负责控制蛇的移动。它首先复制蛇头的位置,然后根据当前方向更新蛇头的坐标,并将新的蛇头添加到蛇的数组中。如果蛇吃到了食物,则生成新的食物,否则,蛇的尾部会被移除,从而保持蛇的长度不变:

function move() {
  // 创建蛇头位置的副本
  const headPosition = Object.assign({}, snake.slice(-1)[0]);

  switch (direction) {
    case "left":
      headPosition.x -= unitLength;
      break;
    case "right":
      headPosition.x += unitLength;
      break;
    case "up":
      headPosition.y -= unitLength;
      break;
    case "down":
      headPosition.y += unitLength;
  }

  // 将新的蛇头添加到数组中
  snake.push(headPosition);

  ctx.fillRect(headPosition.x, headPosition.y, unitLength, unitLength);

  // 检查蛇是否吃到食物
  const isEating =
    foodPosition.x == headPosition.x && foodPosition.y == headPosition.y;

  if (isEating) {
    // 生成新的食物位置
    generateFood();
  } else {
    // 如果蛇没有吃到食物,则移除蛇尾
    tailPosition = snake.shift();

    // 清除尾部在画布上的位置
    ctx.clearRect(tailPosition.x, tailPosition.y, unitLength, unitLength);
  }
}

`turn` 函数

此函数用于改变蛇的移动方向,但它会检查新的移动方向是否与当前方向垂直。例如,当蛇向上或向下移动时,它只能向左或向右转,反之亦然:

function turn(newDirection) {
  switch (newDirection) {
    case "left":
    case "right":
      // 只有当蛇的原始移动方向是向上或向下时,才允许向左或向右转
      if (direction == "up" || direction == "down") {
        direction = newDirection;
      }
      break;
    case "up":
    case "down":
      // 只有当蛇的原始移动方向是向左或向右时,才允许向上或向下转
      if (direction == "left" || direction == "right") {
        direction = newDirection;
      }
      break;
  }
}

`onKeyDown` 函数

此函数是一个事件监听器,用于响应键盘按键事件。它会根据用户按下的箭头键调用相应的`turn`函数来改变蛇的移动方向:

function onKeyDown(e) {
  switch (e.key) {
    case "ArrowDown":
      turn("down");
      break;
    case "ArrowUp":
      turn("up");
      break;
    case "ArrowLeft":
      turn("left");
      break;
    case "ArrowRight":
      turn("right");
      break;
  }
}

`gameLoop` 函数

`gameLoop`函数是游戏的主循环。它会调用`move`函数来移动蛇,调用`checkForCollision`函数来检查碰撞。如果发生碰撞,则停止游戏循环并显示“游戏结束”屏幕:

function gameLoop() {
  move();
  checkForCollision();

  if (collided) {
    clearInterval(timer);
    gameOverScreen.style.display = "flex";
  }
}

启动游戏

最后,添加以下代码来启动游戏:

setUp();
document.addEventListener("keydown", onKeyDown);
let timer = setInterval(gameLoop, 200);

这段代码首先调用`setUp`函数来初始化游戏,然后添加`keydown`事件监听器来处理键盘输入,并使用`setInterval`函数来启动游戏循环。

总结

至此,你的JavaScript代码应该与我GitHub仓库中的代码类似。如果有任何问题,可以仔细检查我的仓库中的代码。接下来,你可能想了解如何在JavaScript中创建图像滑块。