用 JavaScript 写个贪吃蛇

2019/08/29 | 5分钟阅读 | 更新于 2019/08/29

用 JavaScript 写个贪吃蛇

很久以前在学校图书馆里发现一本介绍 Canvas 的书,给出的最简单的一个项目是写个贪吃蛇的游戏。

I. 准备工作

  • 首先找个2D的贪吃蛇游戏玩一玩找找感觉,留意一下游戏的实现细节
  • 打开 VSCode,或者别的你感觉比较趁手的代码编辑器
  • 新建一个空白的HTML文档 index.html 之类的,名字无所谓,把这段东西贴进去
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Snake</title>
    <style>
    #game {
        border: 1px solid #333;
    }
    </style>
</head>
<body>
    <canvas id="game" width="400" height="300"></canvas>
    <script>
        const gameCanvas = document.getElementById('game');
        const ctx = gameCanvas.getContext('2d');

        ctx.fillStyle = 'green';
        ctx.fillRect(10, 20, 100, 200);
    </script>
</body>
</html>

如果你打开创建好的 HTML 文件,会看到一个黑色的框框,里面有个绿色的矩形。这个框框里就是 <canvas>,宽 400 像素,高 300 像素,黑色边框没什么别的用处,纯碎是方便确认画布的位置。框框里的矩形则是用 JavaScript 绘制出来的图形。

HTML 中 <canvas> 是画布,HTML 中一个专门拿来画画绘制图形的元素,widthheight 指定了画布的大小。想要在 canvas 上面画东西,首先你得获取到那个画布(想想看如果你有多个 canvas 元素在页面上是吧),document.getElementById(xxx) 这句代码已经十分直白了,注意到没,代码里的 canvas 上有一句 id="game",所以你就可以通过这个 id 获取到对应的画布。

不过只是获取到画布是不够的,还要拿到它的上下文(context),所以这里用了一句 gameCanvas.getContext('2d')gameCanvas 是前一句定义好的变量,虽然这里通常会顺便写成 canvas 或者 game,不过对于编程没什么概念的话可能会产生困惑。

ctx.fillStyle = 'green';
ctx.fillRect(10, 20, 100, 200);

这两句就很直白了,fill 填充,rect 矩形,style 样式,凑在一起就可以理解了。

II. 准备舞台

首先,请回想一下见到过的贪吃蛇游戏,蛇的身体是分成一个一个小格子的,食物自己是占据一个小格子的,整个地图(画布)都是分成好多个小格子的,每个小格子都是一个放大了的「像素」。我们姑且称它为 cell 好了。

我们可以让一个 cell 的宽高都为 20 像素,这样画布就会有 20列x15行。现在,将 <script></script> 标签中的代码替换为下面这个样子,然后保存并刷新之前的 HTML 文档。

const gameCanvas = document.getElementById('game');
const ctx = gameCanvas.getContext('2d');

const cellSize = 20;
const yCount = gameCanvas.height / cellSize;
const xCount = gameCanvas.width / cellSize;

// 绘制网格
ctx.strokeStyle = '#dddddd';
for (let i = 0; i < xCount; i++) {
    for (let r = 0; r < yCount; r++) {
        ctx.strokeRect(i * cellSize, r * cellSize, cellSize, cellSize);
    }
}

你会看到原本的黑色框框被网格填充了,这个网格和刚刚的边框一样没什么用处,纯粹是方便观察。

#dddddd 是一种颜色的表示方法,是三个十六进制数分别代表红(R)绿(G)蓝(B),三个数字一样的话就是某种程度的灰。

III. 画蛇

画蛇,不过首先得知道蛇是什么样子,以及它是怎么移动的。如果你有思考过,可能会意识到,蛇在移动的时候,其实只有尾巴在动,最后一节的尾巴会 duang 一下子跑到蛇的前面去,其他部分都没有真的动。

然后,蛇的每一节都占据一个格子,我们可以取个坐标。还记得前面的行数和列数嘛,如果坐标都从 0 开始的话,就很简单了,大概这个样子:

列1 列2
0 0 1 0
0 1 1 1

然后我们可以定义一条刚刚开始的两节的蛇。这里用了 10,11,7 是尽可能把它放在中间,不过更合理的做法是用 xCount 和 yCount 去计算合适的位置,我们偷个懒先。

const snake = [
    { x: 10, y: 7 },
    { x: 11, y: 7 },
];

那么怎么画蛇?

// 定义一个函数来画蛇
function drawSnake() { // 姑且继续让 snake 作为全局变量
    snake.forEach(cell => { // cell => {} 是另一种写法更简单的定义函数的写法
        ctx.fillRect(cell.x * cellSize, cell.y * cellSize, cellSize, cellSize);
    });
}

drawSnake(); // 可以这样子调用我们定义的方法

总之现在大概可以看到两个连在一起的方块在画布中间,当前的 JS 代码大概是这个样子:

const gameCanvas = document.getElementById('game');
const ctx = gameCanvas.getContext('2d');

const cellSize = 20;
const yCount = gameCanvas.height / cellSize;
const xCount = gameCanvas.width / cellSize;

const snake = [
    { x:  9, y: 7 },
    { x: 10, y: 7 },
];

drawSnake();

function drawSnake() { // 函数可以定义在任意位置
    snake.forEach(cell => {
        ctx.fillRect(cell.x * cellSize, cell.y * cellSize, cellSize, cellSize);
    });
}

IV. 蛇啊,动起来吧

说起来,我们可能会分不清哪边是蛇头,可以考虑给第一个 cell 改成不同的颜色,不过这不重要。不过呢,蛇的前进方向是重要的,来定义一个好了

// 这些东西最好放在 snake 的前面去
const TOP = 0;
const RIGHT = 1;
const DOWN = 2;
const LEFT = 3;

let dir = LEFT;

现在问题来了,要怎么让蛇动起来呢?没有什么好的办法,需要重绘画布。多久重新回一次呢,无所谓,大概一两百毫秒?久了就会感觉巨卡,短了可能反应不过来。一个比较好的做法是蛇越长间隔越短,相当于难度提高。

JavaScript 里面正好有个方法可以搞这件事⬇️,不过有两个问题,一是作为背景的网格没了,而是蛇没动(当然)。

setInterval(function() {
    drawSnake();
}, 200);

网格好说,我们把那段绘制网格的代码塞进来就好了,不过会变很长,所以可以定义成一个函数,比如叫 drawGrid() 之类的。

setInterval(function() {
    drawGrid();
    drawSnake();
}, 200);

我们还需要一个方法来移动蛇,前面讲过我们不需要改变每一个 cell,而只需要把最后一个 cell 拼到最前面去。

snake.pop() 可以把 snake 这个数组的最后一个丢出来,snake.unshift(x) 则可以在数组的头部插入元素 x。

setInterval(function() {
    ctx.clearRect(0, 0, gameCanvas.width, gameCanvas.height);
    drawGrid();
    moveSnake();
    drawSnake();
}, 200);

// ...

function moveSnake() {
    snake.pop();
    const head = snake[0];
    switch(dir) {
        case UP:
            snake.unshift({ x: head.x, y: head.y - 1 });
            break;
        case RIGHT:
            snake.unshift({ x: head.x + 1, y: head.y });
            break;
        case DOWN:
            snake.unshift({ x: head.x, y: head.y + 1 });
            break;
        case LEFT:
            snake.unshift({ x: head.x - 1, y: head.y });
            break;
    }
}

现在的代码大概是这个样子的

const gameCanvas = document.getElementById('game');
const ctx = gameCanvas.getContext('2d');

const cellSize = 20;
const yCount = gameCanvas.height / cellSize;
const xCount = gameCanvas.width / cellSize;

const UP = 0;
const RIGHT = 1;
const DOWN = 2;
const LEFT = 3;

let dir = LEFT;
const snake = [
    { x:  9, y: 7 },
    { x: 10, y: 7 },
];

drawGrid();

setInterval(function() {
    ctx.clearRect(0, 0, gameCanvas.width, gameCanvas.height);
    drawGrid();
    moveSnake();
    drawSnake();
}, 200);

function moveSnake() {
    snake.pop();
    const head = snake[0];
    switch(dir) {
        case UP:
            snake.unshift({ x: head.x, y: head.y - 1 });
            break;
        case RIGHT:
            snake.unshift({ x: head.x + 1, y: head.y });
            break;
        case DOWN:
            snake.unshift({ x: head.x, y: head.y + 1 });
            break;
        case LEFT:
            snake.unshift({ x: head.x - 1, y: head.y });
            break;
    }
}

function drawGrid() {
    // 绘制网格
    ctx.strokeStyle = '#dddddd';
    for (let i = 0; i < xCount; i++) {
        for (let r = 0; r < yCount; r++) {
            ctx.strokeRect(i * cellSize, r * cellSize, cellSize, cellSize);
        }
    }
}

function drawSnake() {
    snake.forEach(cell => {
        ctx.fillRect(cell.x * cellSize, cell.y * cellSize, cellSize, cellSize);
    });
}

© 2026 香蕉引擎故障报告

🌱 Powered by Hugo with theme Dream.

关于

要怎么介绍自己呢,🤔。

很早以前是作为 Web 前端在学习的,但是工作第一年就成为了全干工程师。喜欢尝试各种东西,什么都会一点。

一直很喜欢 Ebiten 游戏引擎 ,特别简洁,用它做过一些小东西,可以查看这个分类 。另外特别推荐这个木鱼 ,是一个相对完整的小玩意儿,包含手搓的一个简单的 UI 框架;支持鼠标和键盘操作;有多语言和主题切换功能;同时支持 Web 端和客户端。它的源代码在 bin16/wooden-fish

主题

网站基于 Hugo,当前使用的是 hugo-theme-dream 主题的修改版 ,根据我的需要,做了一些对 PaperMod 的兼容。

我自己也写过主题 ,但是没有别人写的好看。

正在从我的笔记中往外搬运内容

等待更新:

  • 从《锈湖》中学了些什么东西
  • 我拿 React 写解谜游戏的经过
  • 基于 Pocketbase 的 Pocket Memos
  • 数独!