从零开始:用 HTML、CSS、JavaScript 制作一个贪吃蛇游戏

举报
yd_282947021 发表于 2026/10/22 09:02:49 2026/10/22
【摘要】 一、项目介绍贪吃蛇是一款经典的小游戏。玩家通过方向键控制蛇的移动,吃掉食物后蛇身会变长,同时游戏得分增加。如果蛇撞到墙壁或者自己的身体,游戏就会结束。本项目具有以下特点:纯前端实现:只使用 HTML、CSS 和 JavaScript。无需图片:使用 Canvas 绘制蛇、食物和游戏背景。无需依赖:不需要安装第三方库,也不需要服务器。支持键盘操作:使用方向键或 WASD 控制移动。完整游戏功能...

一、项目介绍

贪吃蛇是一款经典的小游戏。玩家通过方向键控制蛇的移动,吃掉食物后蛇身会变长,同时游戏得分增加。如果蛇撞到墙壁或者自己的身体,游戏就会结束。

本项目具有以下特点:

  • 纯前端实现:只使用 HTML、CSS 和 JavaScript。

  • 无需图片:使用 Canvas 绘制蛇、食物和游戏背景。

  • 无需依赖:不需要安装第三方库,也不需要服务器。

  • 支持键盘操作:使用方向键或 WASD 控制移动。

  • 完整游戏功能:开始、暂停、重新开始、计分和最高分记录。

二、完整代码

创建一个名为 snake.html 的文件,将下面的代码完整复制进去。


<!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>
    * {
      box-sizing: border-box;
    }

    body {
      margin: 0;
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 20px;
      background: #101827;
      color: #f1f5f9;
      font-family: system-ui, sans-serif;
    }

    .game {
      width: min(100%, 460px);
      text-align: center;
    }

    h1 {
      margin-bottom: 8px;
      letter-spacing: 3px;
    }

    .subtitle {
      color: #94a3b8;
      font-size: 14px;
      margin-bottom: 22px;
    }

    .scores {
      display: flex;
      gap: 12px;
      margin-bottom: 14px;
    }

    .score-box {
      flex: 1;
      padding: 12px;
      background: #1e293b;
      border-radius: 12px;
    }

    .score-box span {
      display: block;
      color: #94a3b8;
      font-size: 12px;
    }

    .score-box strong {
      display: block;
      margin-top: 5px;
      font-size: 24px;
    }

    canvas {
      display: block;
      width: 100%;
      height: auto;
      border: 2px solid #334155;
      border-radius: 12px;
      background: #0b1220;
      box-shadow: 0 12px 40px #0004;
    }

    #status {
      min-height: 24px;
      margin: 14px 0;
      color: #a7f3d0;
      font-size: 14px;
    }

    .buttons {
      display: flex;
      gap: 10px;
    }

    button {
      flex: 1;
      padding: 12px 8px;
      border: 0;
      border-radius: 9px;
      background: #22c55e;
      color: #052e16;
      font-size: 15px;
      font-weight: 700;
      cursor: pointer;
    }

    button:hover {
      filter: brightness(1.1);
    }

    button.secondary {
      background: #334155;
      color: #f8fafc;
    }

    .help {
      margin-top: 18px;
      color: #94a3b8;
      font-size: 12px;
      line-height: 1.9;
    }

    .mobile-controls {
      display: grid;
      grid-template-columns: repeat(3, 58px);
      justify-content: center;
      gap: 7px;
      margin-top: 18px;
    }

    .mobile-controls button {
      min-height: 48px;
      padding: 8px;
      font-size: 20px;
      background: #334155;
      color: white;
    }

    .mobile-controls .empty {
      visibility: hidden;
    }

    @media (min-width: 600px) {
      .mobile-controls {
        display: none;
      }
    }
  </style>
</head>
<body>
  <main class="game">
    <h1>贪吃蛇</h1>
    <div class="subtitle">专注每一次移动,挑战你的最高分</div>

    <div class="scores">
      <div class="score-box">
        <span>当前得分</span>
        <strong id="score">0</strong>
      </div>
      <div class="score-box">
        <span>历史最高分</span>
        <strong id="best">0</strong>
      </div>
    </div>

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

    <div id="status">点击“开始游戏”开启挑战</div>

    <div class="buttons">
      <button id="start">开始游戏</button>
      <button id="pause" class="secondary">暂停</button>
      <button id="restart" class="secondary">重新开始</button>
    </div>

    <div class="mobile-controls">
      <button class="empty"> </button>
      <button data-dir="up">▲</button>
      <button class="empty"> </button>
      <button data-dir="left">◀</button>
      <button data-dir="down">▼</button>
      <button data-dir="right">▶</button>
    </div>

    <div class="help">
      方向键 / WASD 控制移动<br>
      空格键暂停 · 吃掉绿色食物获得分数<br>
      撞到墙壁或自身,游戏结束
    </div>
  </main>

  <script>
    const canvas = document.getElementById("board");
    const ctx = canvas.getContext("2d");
    const scoreEl = document.getElementById("score");
    const bestEl = document.getElementById("best");
    const statusEl = document.getElementById("status");
    const startBtn = document.getElementById("start");
    const pauseBtn = document.getElementById("pause");

    const CELL = 20;
    const SIZE = canvas.width / CELL;

    let snake, food, direction, nextDirection;
    let score = 0;
    let best = loadBest();
    let timer = null;
    let running = false;
    let gameOver = false;
    let paused = false;

    bestEl.textContent = best;

    function loadBest() {
      try {
        return Number(localStorage.getItem("snakeBest")) || 0;
      } catch {
        return 0;
      }
    }

    function saveBest() {
      try {
        localStorage.setItem("snakeBest", String(best));
      } catch {}
    }

    function reset() {
      clearInterval(timer);
      timer = null;
      snake = [
        { x: 10, y: 10 },
        { x: 9, y: 10 },
        { x: 8, y: 10 }
      ];
      direction = { x: 1, y: 0 };
      nextDirection = { ...direction };
      score = 0;
      running = false;
      paused = false;
      gameOver = false;

      scoreEl.textContent = score;
      pauseBtn.textContent = "暂停";
      startBtn.textContent = "开始游戏";
      statusEl.textContent = "准备好了吗?点击开始游戏";
      placeFood();
      draw();
    }

    function placeFood() {
      const empty = [];

      for (let y = 0; y < SIZE; y++) {
        for (let x = 0; x < SIZE; x++) {
          if (!snake.some(part => part.x === x && part.y === y)) {
            empty.push({ x, y });
          }
        }
      }

      if (empty.length === 0) {
        win();
        return;
      }

      food = empty[Math.floor(Math.random() * empty.length)];
    }

    function draw() {
      ctx.fillStyle = "#0b1220";
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      // 绘制网格
      ctx.strokeStyle = "#172338";
      ctx.lineWidth = 1;

      for (let i = 0; i <= SIZE; i++) {
        ctx.beginPath();
        ctx.moveTo(i * CELL, 0);
        ctx.lineTo(i * CELL, canvas.height);
        ctx.stroke();

        ctx.beginPath();
        ctx.moveTo(0, i * CELL);
        ctx.lineTo(canvas.width, i * CELL);
        ctx.stroke();
      }

      // 绘制食物
      if (food) {
        ctx.fillStyle = "#fb7185";
        ctx.beginPath();
        ctx.arc(
          food.x * CELL + CELL / 2,
          food.y * CELL + CELL / 2,
          CELL * 0.36,
          0,
          Math.PI * 2
        );
        ctx.fill();
      }

      // 绘制蛇身
      snake.forEach((part, index) => {
        const gap = 2;
        ctx.fillStyle = index === 0 ? "#86efac" : "#22c55e";
        ctx.beginPath();
        ctx.roundRect(
          part.x * CELL + gap,
          part.y * CELL + gap,
          CELL - gap * 2,
          CELL - gap * 2,
          5
        );
        ctx.fill();

        // 蛇头眼睛
        if (index === 0) {
          ctx.fillStyle = "#14532d";
          const ex = part.x * CELL;
          const ey = part.y * CELL;

          let eyes;
          if (direction.x === 1) {
            eyes = [[13, 6], [13, 14]];
          } else if (direction.x === -1) {
            eyes = [[5, 6], [5, 14]];
          } else if (direction.y === -1) {
            eyes = [[6, 5], [14, 5]];
          } else {
            eyes = [[6, 13], [14, 13]];
          }

          eyes.forEach(([x, y]) => {
            ctx.beginPath();
            ctx.arc(ex + x, ey + y, 2, 0, Math.PI * 2);
            ctx.fill();
          });
        }
      });

      if (paused || gameOver) {
        ctx.fillStyle = "#020617bb";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = "#ffffff";
        ctx.textAlign = "center";
        ctx.font = "bold 28px system-ui";
        ctx.fillText(
          gameOver ? "游戏结束" : "已暂停",
          canvas.width / 2,
          canvas.height / 2
        );
      }
    }

    function tick() {
      direction = { ...nextDirection };

      const head = {
        x: snake[0].x + direction.x,
        y: snake[0].y + direction.y
      };

      const ate = head.x === food.x && head.y === food.y;

      // 未吃到食物时,尾巴本次会移动,因此允许进入原尾部格子
      const body = ate ? snake : snake.slice(0, -1);

      const hitWall =
        head.x < 0 || head.x >= SIZE ||
        head.y < 0 || head.y >= SIZE;

      const hitSelf = body.some(
        part => part.x === head.x && part.y === head.y
      );

      if (hitWall || hitSelf) {
        endGame();
        return;
      }

      snake.unshift(head);

      if (ate) {
        score += 10;
        scoreEl.textContent = score;

        if (score > best) {
          best = score;
          bestEl.textContent = best;
          saveBest();
        }

        placeFood();
        if (gameOver) return;
      } else {
        snake.pop();
      }

      draw();
    }

    function startGame() {
      if (running) return;

      if (gameOver) reset();

      running = true;
      paused = false;
      startBtn.textContent = "游戏进行中";
      statusEl.textContent = "游戏进行中,加油!";
      timer = setInterval(tick, 120);
      draw();
    }

    function togglePause() {
      if (!running && !paused) return;

      if (paused) {
        paused = false;
        running = true;
        timer = setInterval(tick, 120);
        pauseBtn.textContent = "暂停";
        statusEl.textContent = "游戏继续";
      } else {
        clearInterval(timer);
        timer = null;
        running = false;
        paused = true;
        pauseBtn.textContent = "继续";
        statusEl.textContent = "游戏已暂停";
      }

      draw();
    }

    function endGame() {
      clearInterval(timer);
      timer = null;
      running = false;
      paused = false;
      gameOver = true;
      startBtn.textContent = "再玩一次";
      statusEl.textContent = `游戏结束!最终得分:${score}`;
      draw();
    }

    function win() {
      clearInterval(timer);
      timer = null;
      running = false;
      paused = false;
      gameOver = true;
      startBtn.textContent = "再玩一次";
      statusEl.textContent = `恭喜通关!最终得分:${score}`;
      draw();
    }

    function changeDirection(dir) {
      const dirs = {
        up: { x: 0, y: -1 },
        down: { x: 0, y: 1 },
        left: { x: -1, y: 0 },
        right: { x: 1, y: 0 }
      };

      const target = dirs[dir];
      if (!target) return;

      // 禁止直接反向移动
      if (
        target.x + direction.x === 0 &&
        target.y + direction.y === 0
      ) return;

      nextDirection = target;
    }

    document.addEventListener("keydown", event => {
      const keys = {
        ArrowUp: "up",
        w: "up",
        W: "up",
        ArrowDown: "down",
        s: "down",
        S: "down",
        ArrowLeft: "left",
        a: "left",
        A: "left",
        ArrowRight: "right",
        d: "right",
        D: "right"
      };

      if (keys[event.key]) {
        event.preventDefault();
        if (!running && !paused && !gameOver) {
          startGame();
        }
        changeDirection(keys[event.key]);
      }

      if (event.code === "Space") {
        event.preventDefault();
        togglePause();
      }
    });

    document.querySelectorAll("[data-dir]").forEach(button => {
      button.addEventListener("click", () => {
        changeDirection(button.dataset.dir);
        if (!running && !paused && !gameOver) {
          startGame();
        }
      });
    });

    startBtn.addEventListener("click", startGame);
    pauseBtn.addEventListener("click", togglePause);
    document.getElementById("restart").addEventListener("click", reset);

    reset();
  </script>
</body>
</html>

三、如何运行游戏

  1. 新建一个文本文件,命名为 snake.html。

  2. 将上面的完整代码复制到文件中并保存。
  3. 使用 Chrome、Edge 或 Firefox 打开这个 HTML 文件。
  4. 点击“开始游戏”,使用键盘方向键控制蛇移动。

如果你在 Windows 或 WSL 环境中开发,也可以在文件所在目录通过浏览器打开。这个游戏不需要运行 npm install,也不需要启动后端服务。

四、核心代码解析

1. 使用 Canvas 绘制游戏画面

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

Canvas 提供二维绘图能力,可以直接绘制网格、蛇身和食物,无需准备任何图片资源。

2. 使用数组表示蛇的身体

let snake = [
  { x: 10, y: 10 },
  { x: 9, y: 10 },
  { x: 8, y: 10 }
];

数组中的第一个元素代表蛇头,其余元素代表蛇身。每次移动时,在数组头部添加新的蛇头,并删除末尾的蛇身,即可实现移动效果。

3. 实现吃食物和增长

snake.unshift(head);

if (ate) {
  score += 10;
} else {
  snake.pop();
}

当蛇头碰到食物时,蛇身不删除尾部,因此长度增加;否则删除最后一个元素,保持长度不变。

4. 实现游戏循环

timer = setInterval(tick, 120);

每隔 120 毫秒执行一次游戏逻辑,包括移动蛇身、检查碰撞、判断是否吃到食物以及刷新画面。

五、总结

通过这个小项目,我们完成了一个不依赖图片和第三方库的浏览器贪吃蛇游戏,同时学习了 Canvas 绘图、JavaScript 定时器、键盘事件、数组操作和碰撞检测等基础知识。

后续还可以增加难度递增、音效、排行榜、主题切换、触屏滑动和最高分统计等功能,让游戏更加完整。

建议下一步: 将这个小游戏部署到 GitHub Pages 或自己的博客网站中,让访客无需下载文件,直接在线游玩。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。