简介:这份资源是一套面向网页开发初学者的HTML贪吃蛇游戏实战项目,适合刚接触前端、希望通过小游戏理解HTML、CSS与JavaScript协同工作的学习者。项目围绕游戏区域搭建、分数显示、键盘事件监听、蛇身位置更新、碰撞检测与食物随机生成等核心逻辑展开,帮助读者把静态页面与动态交互串联起来。压缩包共3个文件,包含1个html主页面、1张jpg背景图和1张png素材图,整体约86KB,体积轻巧,便于直接运行和二次修改。目前已有1453人学习下载,说明该案例在入门练习中具有一定参考价值。读者可从中获得完整的游戏页面结构、基础样式布局思路以及JavaScript驱动游戏循环的实现方式,并在此基础上尝试加入音效、难度分级或CSS3动画等扩展功能,适合作为前端入门阶段巩固DOM操作与逻辑思维的练手素材。
1. 用 HTML 写贪吃蛇:一个文件就能跑起来的经典练手项目
很多人第一次接触前端,都是从「用 HTML 做贪吃蛇」开始的。它不像框架项目那样一上来就配环境、装依赖,而是把 HTML、CSS、JavaScript 三件套塞进一个文件里,双击就能玩。这份资源就是这样一个完整可运行的贪吃蛇小游戏,核心逻辑全部用原生 JavaScript 实现,没有引入任何第三方库,适合刚学完 HTML 基础标签、想找个东西练手的开发者,也适合想拿它当教学案例、二次改成其他网格类游戏的人。它解决的不是业务问题,而是「怎么把键盘事件、定时器、碰撞检测、状态管理串成一个能玩的东西」这个问题。下面我按拆项目的思路,把它从结构到代码再到改造讲一遍。
2. 项目结构与技术选型:为什么一个 HTML 文件就够了
2.1 目录结构与文件职责
这份资源的结构非常扁平,常见做法就是一个主文件加一个说明文档:
snake-game/ ├── index.html # 页面结构 + 样式 + 游戏逻辑,全部内联 └── README.md # 玩法说明和改造提示index.html里通常分三块:<style>负责画布和分数面板的样式,<canvas>负责渲染游戏画面,<script>负责全部逻辑。之所以不拆成game.js、style.css,是因为这个项目的定位就是「零配置、双击即玩」,拆文件反而增加了新手的心智负担。等你二次开发到一定规模,再拆也不迟。
2.2 技术选型:Canvas 还是 DOM 网格
贪吃蛇的渲染有两条路:用 DOM 的div拼网格,或者用<canvas>画。这份资源用的是 Canvas,原因很直接——蛇身移动时如果频繁增删 DOM 节点,浏览器重排开销大,蛇一长就卡;Canvas 每帧整体重绘,逻辑更干净。
核心 API 就几个:
| API | 作用 |
|---|---|
canvas.getContext('2d') | 拿到 2D 绘图上下文 |
ctx.fillRect(x, y, w, h) | 画蛇身和食物 |
ctx.clearRect() | 每帧清屏 |
setInterval/requestAnimationFrame | 驱动游戏循环 |
keydown事件 | 接收方向键输入 |
提示:用
setInterval控制速度最简单,但帧率不稳定;进阶做法是requestAnimationFrame配合时间戳累加,后面改造章节会讲。
2.3 游戏循环与状态设计
整个游戏的状态可以用几个变量描述清楚:蛇身坐标数组、当前方向、食物坐标、分数、游戏是否结束。常见做法是把它们放在脚本顶部统一声明,方便调试时一眼看到全貌:
const canvas = document.getElementById('game'); const ctx = canvas.getContext('2d'); const gridSize = 20; // 每个格子 20px const tileCount = canvas.width / gridSize; // 横向格子数 let snake = [{ x: 10, y: 10 }]; // 蛇身,头部在数组第一个 let direction = { x: 0, y: 0 }; // 当前移动方向 let food = { x: 15, y: 10 }; // 食物坐标 let score = 0; let gameOver = false;这里gridSize和tileCount决定了棋盘大小,改这两个值就能改难度。蛇身用数组存,头部unshift新坐标、尾部pop掉旧坐标,就实现了「移动」的视觉效果,这是贪吃蛇最核心的一个技巧。
3. 核心逻辑拆解:从键盘输入到碰撞检测的完整链路
3.1 键盘事件与方向控制
方向控制最容易翻车的地方是「反向自杀」——蛇正在向右走,你按左键,它立刻掉头撞上自己。解决办法是记录当前方向,禁止输入与当前方向相反的键:
document.addEventListener('keydown', (e) => { switch (e.key) { case 'ArrowUp': if (direction.y === 0) direction = { x: 0, y: -1 }; break; case 'ArrowDown': if (direction.y === 0) direction = { x: 0, y: 1 }; break; case 'ArrowLeft': if (direction.x === 0) direction = { x: -1, y: 0 }; break; case 'ArrowRight': if (direction.x === 0) direction = { x: 1, y: 0 }; break; } });direction.y === 0这个判断的意思是:只有当前不在垂直移动时,才允许改成垂直方向。这样向右走时按左键会被忽略,避免瞬间掉头。参数x、y的取值只有 -1、0、1,分别代表左/右、上/下。
3.2 游戏主循环与移动逻辑
每一帧要做四件事:算新头部坐标、判断撞墙撞自己、判断是否吃到食物、重绘画面。顺序不能乱,先判死再判吃,否则会出现「死了还在加分」的玄学 bug:
function gameLoop() { if (gameOver) return; // 1. 计算新头部位置 const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y }; // 2. 撞墙检测 if (head.x < 0 || head.x >= tileCount || head.y < 0 || head.y >= tileCount) { gameOver = true; return; } // 3. 撞自己检测 for (let i = 0; i < snake.length; i++) { if (snake[i].x === head.x && snake[i].y === head.y) { gameOver = true; return; } } // 4. 吃到食物:头部入队,不删尾部(蛇变长) if (head.x === food.x && head.y === food.y) { score += 10; snake.unshift(head); placeFood(); // 重新随机生成食物 } else { snake.unshift(head); snake.pop(); // 没吃到,尾部删掉,长度不变 } draw(); }snake.unshift(head)把新头插到数组最前,snake.pop()删掉最后一节,整体看起来就是蛇在往前爬。吃到食物时不pop,蛇就长了一节。placeFood()负责在棋盘内随机一个不与蛇身重叠的坐标。
3.3 渲染与食物生成
渲染函数每帧先清屏,再画食物,最后画蛇。食物用不同颜色区分,蛇头可以单独加深一点,视觉上更清楚:
function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 画食物 ctx.fillStyle = '#e74c3c'; ctx.fillRect(food.x * gridSize, food.y * gridSize, gridSize - 2, gridSize - 2); // 画蛇 snake.forEach((seg, i) => { ctx.fillStyle = i === 0 ? '#2ecc71' : '#27ae60'; ctx.fillRect(seg.x * gridSize, seg.y * gridSize, gridSize - 2, gridSize - 2); }); // 更新分数显示 document.getElementById('score').textContent = '得分:' + score; } function placeFood() { let newFood; do { newFood = { x: Math.floor(Math.random() * tileCount), y: Math.floor(Math.random() * tileCount) }; } while (snake.some(s => s.x === newFood.x && s.y === newFood.y)); food = newFood; }gridSize - 2是给每个格子留 2px 间隙,看起来像一块块独立的方块,比实心一片好看。placeFood里的do...while保证食物不会生成在蛇身上,否则会出现「食物被蛇盖住吃不到」的情况。
3.4 启动与重开
最后把定时器和重开按钮接上,整个游戏就闭环了:
let timer = setInterval(gameLoop, 150); // 150ms 一帧,数字越小越快 document.getElementById('restart').addEventListener('click', () => { snake = [{ x: 10, y: 10 }]; direction = { x: 0, y: 0 }; score = 0; gameOver = false; placeFood(); draw(); });setInterval的第二个参数 150 就是速度,改成 100 会明显变快,改成 250 就慢悠悠。重开时把所有状态重置回初始值,注意direction要归零,否则蛇会带着上一局的方向直接冲出去。
4. 避坑与常见问题排查
4.1 按方向键页面跟着滚动
现象:玩游戏时按上下键,整个网页跟着上下滚。原因:方向键默认会触发页面滚动。解决:在keydown回调里对方向键调用e.preventDefault(),或者在 CSS 里给body加overflow: hidden。
4.2 蛇移动时一顿一顿的
现象:蛇走起来不流畅,像卡帧。原因:setInterval的间隔和渲染节奏不匹配,或者间隔设得太大。解决:把间隔调到 100~150ms,进阶做法是改用requestAnimationFrame加时间累加器,让移动和刷新解耦。
4.3 食物生成在蛇身上
现象:偶尔食物刷出来就看不见,或者刚吃到又消失。原因:placeFood没有排除蛇身坐标。解决:用do...while循环反复随机,直到坐标不在snake数组里为止,上面 3.3 的代码已经处理了。
4.4 连续快速按键导致反向撞死
现象:手速快的时候,蛇会突然掉头撞自己。原因:一帧内收到多个方向键,direction被连续覆盖。解决:除了 3.1 的反向判断,还可以加一个「每帧只接受一次方向变更」的标志位,或者用队列缓存输入。
4.5 游戏结束后定时器还在跑
现象:撞墙后画面还在动,分数还在涨。原因:gameLoop里gameOver判断没拦住,或者定时器没清。解决:在gameOver = true之后clearInterval(timer),重开时再重新setInterval。
5. 二次开发:从贪吃蛇改成你自己的网格游戏
这份资源最大的价值不是贪吃蛇本身,而是它那套「网格 + 循环 + 状态数组」的骨架。你把这套骨架抽出来,改几个地方就能变成推箱子、吃豆人、扫雷。我一般会先动这三个点:
第一,把snake数组换成你要操作的对象。比如推箱子,snake变成箱子坐标数组,direction变成玩家移动方向,碰撞检测从「撞自己」改成「撞墙和撞箱子」。
第二,把gameLoop的驱动方式换掉。贪吃蛇是定时自动走,推箱子是按键才走,所以把setInterval去掉,改成在keydown里直接调move()再draw()。
第三,把渲染层抽象出来。现在draw()里写死了画蛇和食物,你可以改成drawEntity(x, y, color)通用函数,不同游戏传不同参数。
改造完记得验证三件事:边界坐标有没有越界、状态重置有没有漏项、定时器有没有重复创建。我踩过的坑是重开时忘了clearInterval,结果每重开一次就多一个定时器,蛇越跑越快,查了半天才发现是定时器叠加。从那以后我每次写带循环的游戏,都强制在启动前先清一次定时器,再重新创建。希望这份拆解帮到你,拿到文件先跑起来,再照着改一版属于自己的网格游戏。
本文还有配套的精品资源,点击获取