1. 整个简洁版的贪吃蛇
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>贪吃蛇</title>
</head>
<body style="height: 90vh; display: flex; justify-content: center; align-items: center;">
<canvas id="gameBox" width="400" height="400" style="background-color: black">对不起,您的浏览器不支持canvas</canvas>
<script>
var snake = [41, 40],
direction = 1,
food = 53,
n,
box = document.getElementById('gameBox').getContext('2d');
function draw(seat, color) {
box.fillStyle = color;
box.fillRect(seat % 20 * 20 + 1, ~~(seat / 20) * 20 + 1, 18, 18);
}
document.onkeydown = function (evt) {
direction = snake[1] - snake[0] == (n = [-1, -20, 1, 20][(evt || event).keyCode - 37] || direction) ? direction : n;
};
(function () {
snake.unshift(n = snake[0] + direction);
if (snake.indexOf(n, 1) > 0 || n < 0 || n > 399 || direction == 1 && n % 20 == 0 || direction == -1 && n % 20 == 19) {
return alert(`GAME OVER! Mark: ${snake.length - 1}`);
}
draw(n, "lime");
if (n == food) {
if (snake.length == 400) return alert("NB!!!!");
while (snake.indexOf(food = ~~(Math.random() * 400)) > 0);
draw(food, "yellow");
} else {
draw(snake.pop(), "black");
}
setTimeout(arguments.callee, 200);
})(draw(food, "yellow"));
</script>
</body>
</html>

2. 解析
2.1 变量
box = document.getElementById('gameBox').getContext('2d')
画布对象,用于画图
- 游戏地图是一个20X20的方格,这里将二维降为一维,以0--399标识每个方格
snake = [41, 40]
存储蛇的位置节点,第0个节点是蛇头,初始有两个节点
direction = 1
当前移动方向,1表示向右,-1表示向左,20表示向下,-20表示向上
food = 53
当前食物位置
n
与下次移动的位置有关,主要用于检测
2.2 画图方法
function draw(seat, color) {
box.fillStyle = color;
box.fillRect(seat % 20 * 20 + 1, ~~(seat / 20) * 20 + 1, 18, 18);
}
- 蛇和食物都是方格,只要区分位置和颜色就行
seat
表示一维的位置,需要转成二维的实际像素位置
seat % 20 * 20 + 1
,x轴实际像素位置
- 第一个20表示每行20个方格,
%20
取余获取在当前行第几个
- 第二个20表示每个方格在x轴方向占20px,
*20
获取在x轴方向的实际像素位置
+1
是方格间隔1px,用于视觉区分方格
~~(seat / 20) * 20 + 1
,y轴实际像素位置
~~
将一个数字取反两次,可以获取数字整数部分
- 第一个20表示每行20个方格,
~~(seat / 20)
取整获取在当前第几行
- 第二个20表示每个方格在y轴方向占20px,
*20
获取在y轴方向的实际像素位置
+1
是方格间隔1px,用于视觉区分方格
18, 18
,因为上下左右都有1px的间隙,所以20X20变成了18X18
2.3 监听键盘
document.onkeydown = function (evt) {
direction = snake[1] - snake[0] == (n = [-1, -20, 1, 20][(evt || event).keyCode - 37] || direction) ? direction : n;
};
onkeydown
,在页面上监听键盘按下事件
(evt || event).keyCode
,兼容获取按下的按键的数字代码
- 左上右下代码,
('ArrowLeft',37)
('ArrowUp',38)
('ArrowRight',39)
('ArrowDown',40)
(evt || event).keyCode - 37
,将数字码37,38,39,40
转为0,1,2,3
[-1, -20, 1, 20][(evt || event).keyCode - 37]
,将按键码转为游戏方向,如果是其他按键,则结果为undefined
(n = [-1, -20, 1, 20][(evt || event).keyCode - 37] || direction)
,获取用户按键方向或者维持原方向,并赋值给n
snake[1] - snake[0] == (n = [-1, -20, 1, 20][(evt || event).keyCode - 37] || direction)
简化:snake[1] - snake[0] == n
,当前方向是否和上次方向一致
direction = snake[1] - snake[0] == (n = [-1, -20, 1, 20][(evt || event).keyCode - 37] || direction) ? direction : n;
简化:direction = snake[1] - snake[0] == n ? direction : n
,如果当前方向和上次方向不一致,则改变方向
2.4 主程序
(function () {
snake.unshift(n = snake[0] + direction);
if (snake.indexOf(n, 1) > 0 || n < 0 || n > 399 || direction == 1 && n % 20 == 0 || direction == -1 && n % 20 == 19) {
return alert(`GAME OVER! Mark: ${snake.length - 1}`);
}
draw(n, "lime");
if (n == food) {
if (snake.length == 400) return alert("NB!!!!");
while (snake.indexOf(food = ~~(Math.random() * 400)) > 0);
draw(food, "yellow");
} else {
draw(snake.pop(), "black");
}
setTimeout(arguments.callee, 200);
})(draw(food, "yellow"));
2.4.1 开始执行
- 主体简化:
(function(){})()
,自执行函数,定义一个无名函数并立刻执行
(function(){})(draw(food, "yellow"))
,执行函数时传了一个参数,其实就是在执行函数之前执行draw(food, "yellow")
画食物
2.4.2 移动并检测
snake.unshift(n = snake[0] + direction);
添加下一步移动的位置为蛇头
snake.indexOf(n, 1) > 0
忽略第一个,检查后面有没有与蛇头位置一样的,即检查有没有撞身体
n < 0 || n > 399
有没有上下越界
direction == 1 && n % 20 == 0
向右但下一步的位置在最左边,即向右越界
direction == -1 && n % 20 == 19
向左但下一步的位置在最右边,即向左越界
return alert(
GAME OVER! Mark: ${snake.length - 1});
如果下一步非法,结束并打印蛇的长度
draw(n, "lime");
画出蛇头下一步出现的位置
n == food
如果蛇头位置是食物位置,即吃到食物,不用移动蛇尾,即长度加1
if (snake.length == 400) return alert("NB!!!!");
蛇的长度铺满地图,完美成功,结束游戏
while (snake.indexOf(food = ~~(Math.random() * 400)) > 0);
查找一个空位生成食物
draw(food, "yellow")
画出食物
draw(snake.pop(), "black");
如果蛇头位置不是食物位置,则移动蛇尾,即删除蛇尾,并将蛇尾位置画为地图颜色
2.4.3 循环执行
arguments
这是函数内部的一个与当前函数有关的对象
arguments.callee
可以引用函数本身
setTimeout(arguments.callee, 200)
200毫秒后调用函数,可以当做移动速度
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?