用 Canvas 实现一个极简贪食蛇
前言
笔者曾经在学习 TS 时写过一个贪食蛇小游戏,但那是基于 DOM 的,代码较为啰唆(毕竟是用来学习基于静态类型的 TS)。这次无意间看见网上某位高人写的贪食蛇小游戏,寥寥几行代码搞定,叹为观止。初看代码,是很大程度地利用了 JS 弱语言的灵活度,以少量代码做了大量的工作,是真正体现了 JS 在脚本领域无人能敌的强大本领。
先来看看 👉 Demo。
接下来晒下代码,然后做一些思路解读,以便于理解其原理。
首先用 HTML 和 CSS 布置好画布(Canvas):
1 |
|
1 | body { |
通过以上代码我们在页面中央开辟了一块 400 × 400 像素的黑底画布。以 20 × 20 做为一个基本单位,将画布划分为 20 行 20 列的方阵。如图所示:
大体的思路就是用绿色来填充这些小格子来表示蛇身,用黄色的一格来代表随机出现的食物。蛇身要动,要有个定时器,按一定的频率去更新画布。
1 | /** @type {HTMLCanvasElement} */ |
通常这种情况下我们都拿二维数组来代表一个点,或一个格子,比如 [0, 0]
就是左上角第一个格子。但这次不一样,作者是按从左到右,至上而下,依次来标记这些格子的,这样我们就标记出了 0~399 个格子。
画蛇不添足
1 | let snake = [41, 40] |
蛇身用一维数组来表示即可。food
代表食物出现的位置,direction
表示蛇头下一次运动的转向位移量。最有意思的是这个 n
,它是用来记录蛇身单位时间内的移动位移,也就是蛇运动动画当前帧的绘制目标格子。看到这一行:
1 | snake.unshift((n = snake[0] + direction)) |
这里是两步操作:
- 先将蛇头根据方向转向获取到下一步的位移量,赋值给
n
; - 蛇身把下一步的位移
n
做为新的蛇头,加入到一维数组里。
这里还比较好理解。那么接下来看到如何在 canvas 上画格子:
1 | const draw = (seat, color) => { |
根据 20 × 20 的划分标记,我们通过 seat / 20
取整来得到行,seat % 20
来获得列。行和列应该都是 0~19 之间的整数。如果用 row
和 col
分别表示其行和列的结果,按道理应该是这般绘制:box.fillRect(col * 20, row * 20, 20, 20)
——这是画满格的情况。留个一像素的内边距,就是原示例中的写法。
1 | if (n == food) { |
后续 n == food
时表示蛇吃到食物,那就进入到再随机生成食物的逻辑。结合 else 的逻辑,通篇看下来现在我们可以把在一帧内的蛇身操作放在一起看了:
1 | // next step |
direction
direction
的计算也颇为巧妙:
1 | document.onkeydown = (evt) => { |
数组 [-1, -20, 1, 20]
做为四个方向的位移量1,而 (evt || event).keyCode - 37
做为其索引值,按不同方向键可以取到其对应的偏移量。
1 | [-1, -20, 1, 20][(evt || event).keyCode - 37] || direction |
这里的巧妙之处在于如果按下的按键不是方向键,在数组中将得不到对应的值。所以 ...|| direction
这步操作,可以使得 n
取到原来 direction
的值,而不是 undefined
。所以此时 n
的取值,要么是数组 [-1, -20, 1, 20]
中的某个值,要么就是前一个 direction
的值。
1 | snake[1] - snake[0] == ... ? direction : n |
我们已经能很完美地拿到 n
的值了,那上面的这个三元判断又是怎么回事呢?答案是防反转。就是当按键方向与蛇运动方向相反时,如果没有这个判断的话,就会错乱。snake[1] - snake[0]
可视为当前的反方向,即 -direction
,如果此时 n
真的取值为 -direction
,则需将其矫正为 direction
。
条件判断
好了,其它就较为简单了。
1 | // GAME OVER 的判断条件 |
1 | while (snake.indexOf((food = ~~(Math.random() * 400))) > 0); |
在随机生成 food 位置的同时,也避免了生成位与蛇身重合的问题。
IIFE
看到最后一部分:
1 | !(function () { |
这是一个立即执行函数(IIFE),这里有介绍。
顺便提一下其中的 arguments.callee
,指的是:
引用参数所属的当前执行函数 —— MDN web docs
所以这里 setTimeout
就能让这段函数不停地执行下去。当然我们也可以换作 setInterval
来实现。
- 1.分别为:左、上、右、下。↩