
简介这是一份面向网页开发初学者与前端练习者的HTML贪吃蛇游戏实战资源围绕HTML、CSS与JavaScript三者的协同工作展开帮助读者理解如何用标准标记语言搭建游戏界面并借助脚本实现蛇的移动、食物生成、碰撞检测与分数更新等核心逻辑。压缩包共3个文件包含1个html主页面、1张jpg背景图与1张png素材图整体约86KB体积轻巧便于直接运行与二次修改。资源以静态页面结构为基础配合外部脚本完成键盘事件监听、定时器状态刷新与DOM动态更新同时预留了CSS3动画优化与难度扩展空间。目前已有1452人学习下载适合希望从零完成一个可交互小项目、巩固前端基础与逻辑思维训练的读者参考。1. 用 HTML 做贪吃蛇为什么我劝你先别急着写 canvas很多人第一次听到「用 html 做贪吃蛇」脑子里蹦出来的画面是打开记事本、敲几行!doctype html html langzh-cn head meta charsetutf-8然后蛇就动起来了。真动手才发现蛇要么穿墙、要么吃到食物不变长、要么按一下方向键直接反向撞死自己。问题不在 HTMLHTML 只负责搭骨架真正让贪吃蛇跑起来的是 CSS 的网格布局和 JavaScript 的定时循环。这篇笔记讲的就是怎么用最朴素的三件套——html➕css➕js 基础语法——把一个能玩、能改、能扩展的贪吃蛇做出来顺带把新手最容易翻车的地方一次说清。适合刚学完 DOM 操作、想找个完整小项目练手的人也适合想拿它当教学案例的培训场景。读完你能拿到一份可复现的代码结构知道每个参数为什么这么设以及哪些坑我替你踩过了。2. 贪吃蛇的骨架网格、状态与渲染循环怎么定2.1 为什么用 CSS Grid 而不是 canvas选型这件事决定了后面写代码的手感。canvas 画贪吃蛇当然可以一个fillRect就能画一格性能也好但它的代价是所有东西都得自己画网格线、蛇身、食物、分数全是一堆坐标计算。对刚接触 html网页制作 的人来说canvas 的坐标系和重绘逻辑会额外增加一层心智负担调试时你看到的是一张画布不是一个个 DOM 节点想单独改某一格的颜色都费劲。CSS Grid 的思路不一样。它把游戏区域切成固定数量的格子蛇身和食物都是真实的 DOM 元素加个 class 就能改样式浏览器 DevTools 里能直接点中某一节蛇身看它的位置。代价是格子多了以后 DOM 节点数量上升性能不如 canvas。但对一个 20×20 的棋盘来说最多也就 400 个格子现代浏览器完全扛得住。常见做法是棋盘用 Grid蛇和食物用绝对定位或 Grid 单元格填充这样既保留了可调试性又不用手写一堆坐标。我一般会这样定结构外层一个#board容器用display: grid加grid-template-columns: repeat(20, 1fr)切成 20 列行数同理。每个格子是一个div但不需要一开始就生成 400 个而是用 JS 动态生成方便后面根据难度调整棋盘大小。蛇和食物不直接操作格子而是维护一份坐标数组渲染时把对应坐标的格子标记出来。这样逻辑和视图就分开了改渲染方式不影响游戏规则。2.2 游戏状态该存哪些字段状态设计是贪吃蛇的核心。很多人写着写着发现蛇身对不上、食物刷在蛇身上、方向键连按出问题根源都是状态没设计好。我一般会维护这么几个字段字段类型说明snake数组蛇身坐标[{x, y}, ...]索引 0 是蛇头direction对象当前移动方向{x: 1, y: 0}表示向右nextDirection对象缓冲方向避免同一帧内连按导致反向food对象食物坐标{x, y}score数字当前得分speed数字每步间隔毫秒数timer定时器 ID用于暂停和重置isRunning布尔游戏是否进行中这里最关键的是direction和nextDirection分开。如果只用一个方向变量玩家在一步之内快速按「上」再按「左」当蛇当前向右时按「上」合法但紧接着按「左」时如果直接改方向蛇就会在同一帧内先转上再转左等于原地掉头撞自己。用nextDirection做缓冲每步开始时才把nextDirection赋给direction就能避免这个问题。这是血泪经验不分开写迟早会遇到。2.3 渲染循环setInterval 还是 requestAnimationFrame贪吃蛇的移动是离散的一格一格跳所以用setInterval最直观。requestAnimationFrame更适合连续动画用它做贪吃蛇反而要自己算时间累积绕远了。我一般用setInterval间隔就是speed默认 150 毫秒一步难度高了调到 80 甚至 60。但setInterval有个坑如果某一步的逻辑执行时间超过了间隔定时器会堆积蛇会突然连跳好几格。贪吃蛇逻辑很轻一般不会超时但如果你在里面做了复杂计算或频繁 DOM 操作就要注意。稳妥做法是每步开始时先clearInterval再重新设或者用递归setTimeout代替。我倾向后者因为递归setTimeout能保证上一步执行完才排下一步不会堆积。function gameLoop() { if (!state.isRunning) return; step(); // 执行一步移动逻辑 render(); // 重新渲染棋盘 state.timer setTimeout(gameLoop, state.speed); }这段代码里step()负责更新蛇的坐标、判断吃食物和碰撞render()负责把状态映射到 DOM。state.speed就是每步间隔改这个值就能调难度。注意gameLoop开头判断isRunning暂停时直接返回不会继续排定时器。恢复时重新调用gameLoop即可。这种写法比setInterval多一层函数调用但换来了不会堆积的确定性值得。3. 从零写一个能跑的贪吃蛇HTML 结构、CSS 网格与 JS 逻辑3.1 HTML 骨架与 meta 该写什么先看 HTML 部分。一个完整的贪吃蛇页面不需要多复杂但!doctype html html langzh-cn head meta charsetutf-8这几行必须写对。charsetutf-8保证中文不乱码langzh-cn对屏幕阅读器和搜索引擎友好。viewport那行别漏不然手机上棋盘会缩成一团。!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title贪吃蛇/title link relstylesheet hrefstyle.css /head body div classgame div classhud span得分b idscore0/b/span button idpauseBtn暂停/button button idresetBtn重开/button /div div idboard classboard/div /div script srcgame.js/script /body /html结构就三块计分板、棋盘、脚本。棋盘不预生成格子留给 JS 动态创建这样改棋盘尺寸只改一个常量。按钮用原生button不引框架保持零依赖。hud里放得分和两个按钮暂停和重开是最常用的操作放显眼位置。3.2 CSS Grid 把棋盘切成格子CSS 部分的核心是棋盘。用 Grid 切格子每个格子用aspect-ratio: 1保证正方形这样棋盘不管屏幕多宽都能自适应。.board { display: grid; grid-template-columns: repeat(20, 1fr); grid-template-rows: repeat(20, 1fr); width: min(90vw, 480px); aspect-ratio: 1; background: #1a1a2e; border: 2px solid #333; gap: 1px; } .cell { background: #16213e; } .cell.snake { background: #4ade80; border-radius: 3px; } .cell.head { background: #22c55e; } .cell.food { background: #ef4444; border-radius: 50%; }grid-template-columns: repeat(20, 1fr)把宽度均分 20 份行同理。width: min(90vw, 480px)让棋盘在手机上占 90% 宽度桌面上最大 480px配合aspect-ratio: 1保持正方形。gap: 1px让格子之间有细缝视觉上能看清网格。蛇身用绿色蛇头深一点食物红色圆形对比明显。这些颜色值可以随便改不影响逻辑。注意.cell的background是默认底色蛇和食物通过追加 class 覆盖。这样渲染时只需要增删 class不用改内联样式性能更好也方便用 CSS 过渡做简单动画。3.3 JS 初始化生成棋盘、放蛇、刷食物JS 部分从初始化开始。先把常量定好棋盘尺寸、初始蛇长、初始速度都放这里后面改难度只动这几个数。const COLS 20; const ROWS 20; const INIT_LENGTH 3; const INIT_SPEED 150; const boardEl document.getElementById(board); const scoreEl document.getElementById(score); const state { snake: [], direction: { x: 1, y: 0 }, nextDirection: { x: 1, y: 0 }, food: null, score: 0, speed: INIT_SPEED, timer: null, isRunning: false, }; function initBoard() { boardEl.innerHTML ; for (let y 0; y ROWS; y) { for (let x 0; x COLS; x) { const cell document.createElement(div); cell.className cell; cell.dataset.x x; cell.dataset.y y; boardEl.appendChild(cell); } } } function initSnake() { state.snake []; const startY Math.floor(ROWS / 2); for (let i 0; i INIT_LENGTH; i) { state.snake.push({ x: INIT_LENGTH - 1 - i, y: startY }); } state.direction { x: 1, y: 0 }; state.nextDirection { x: 1, y: 0 }; } function spawnFood() { const empty []; for (let y 0; y ROWS; y) { for (let x 0; x COLS; x) { if (!state.snake.some(s s.x x s.y y)) { empty.push({ x, y }); } } } if (empty.length 0) return null; state.food empty[Math.floor(Math.random() * empty.length)]; return state.food; }initBoard生成 400 个格子每个格子用dataset存坐标方便后面查找。initSnake把蛇放在中间行蛇头在右初始长度 3。spawnFood先收集所有空格子再随机选一个这样食物绝不会刷在蛇身上。如果空格子为零说明蛇填满了棋盘返回null表示胜利这个边界要处理。state.snake索引 0 是蛇头移动时在头部插入新坐标尾部根据是否吃到食物决定是否删除。这个设计让「吃食物变长」变成「不删尾部」逻辑很干净。3.4 一步移动碰撞检测与吃食物判定step()是游戏的核心每调用一次蛇前进一格。要处理三件事根据方向算出新蛇头、判断撞墙撞自己、判断吃食物。function step() { state.direction state.nextDirection; const head state.snake[0]; const newHead { x: head.x state.direction.x, y: head.y state.direction.y, }; // 撞墙判定 if (newHead.x 0 || newHead.x COLS || newHead.y 0 || newHead.y ROWS) { return gameOver(); } // 撞自己判定注意尾巴这一格在没吃食物时会移走可以不算 const willEat state.food newHead.x state.food.x newHead.y state.food.y; const bodyToCheck willEat ? state.snake : state.snake.slice(0, -1); if (bodyToCheck.some(s s.x newHead.x s.y newHead.y)) { return gameOver(); } state.snake.unshift(newHead); if (willEat) { state.score 10; scoreEl.textContent state.score; if (state.speed 60) state.speed - 5; spawnFood(); } else { state.snake.pop(); } }撞墙判定直接看新蛇头坐标是否越界。撞自己判定有个细节如果这一步不吃食物尾巴会移走所以新蛇头撞到当前尾巴的位置是合法的检查时要把尾巴排除。willEat为真时尾巴不移才需要检查整条蛇。这个边界很多人写错导致蛇贴着尾巴走时莫名死亡。吃到食物后加分、提速、刷食物。提速每次减 5 毫秒最低 60避免快到没法玩。分数每次加 10简单直接。spawnFood在蛇变长后调用保证食物不刷在蛇身上。3.5 渲染与键盘控制把状态映射到格子渲染函数负责把state映射到 DOM。先清掉所有格子的蛇和食物 class再按当前状态重新标记。function render() { const cells boardEl.children; for (let i 0; i cells.length; i) { cells[i].classList.remove(snake, head, food); } state.snake.forEach((seg, i) { const idx seg.y * COLS seg.x; cells[idx].classList.add(snake); if (i 0) cells[idx].classList.add(head); }); if (state.food) { const idx state.food.y * COLS state.food.x; cells[idx].classList.add(food); } }格子是按行优先生成的所以索引是y * COLS x。先全部清 class再标记蛇和食物。蛇头额外加headclass 区分颜色。这种全量重绘在 400 格下完全没问题不用做增量更新。键盘控制要处理方向键和 WASD同时防止反向。const keyMap { ArrowUp: { x: 0, y: -1 }, ArrowDown: { x: 0, y: 1 }, ArrowLeft: { x: -1, y: 0 }, ArrowRight: { x: 1, y: 0 }, w: { x: 0, y: -1 }, s: { x: 0, y: 1 }, a: { x: -1, y: 0 }, d: { x: 1, y: 0 }, }; document.addEventListener(keydown, e { const dir keyMap[e.key]; if (!dir) return; e.preventDefault(); // 不能反向新方向与当前方向相反时忽略 if (dir.x -state.direction.x dir.y -state.direction.y) return; state.nextDirection dir; });keyMap把按键映射到方向向量方向键和 WASD 都支持。preventDefault阻止方向键滚动页面。反向判定用当前direction而不是nextDirection因为nextDirection可能还没生效。这样连按也不会掉头。启动和暂停逻辑function start() { if (state.isRunning) return; state.isRunning true; gameLoop(); } function pause() { state.isRunning false; clearTimeout(state.timer); } function reset() { pause(); state.score 0; state.speed INIT_SPEED; scoreEl.textContent 0; initSnake(); spawnFood(); render(); start(); }start里判断isRunning防止重复启动。pause清掉定时器。reset重置所有状态后重新开始。初始化时调用initBoard、initSnake、spawnFood、render再绑定按钮事件即可。4. 贪吃蛇最容易翻车的 5 个地方排查与避坑4.1 蛇一按方向键就反向撞死现象是蛇正常向右走玩家按左键蛇立刻掉头撞到自己。原因是方向判断直接改了direction没有缓冲。解决方法是引入nextDirection按键只改nextDirection每步开始时才赋给direction并且反向判定用direction而不是nextDirection。这样同一帧内连按也不会出问题。4.2 食物刷在蛇身上导致蛇瞬间变长现象是食物偶尔出现在蛇身中间蛇经过时直接吃掉或者食物根本看不见。原因是spawnFood只随机了坐标没排除蛇身。解决方法是先遍历所有格子收集不在蛇身上的空格再从空格里随机选。棋盘快满时还要处理空格为零的情况返回null并判定胜利。4.3 定时器堆积导致蛇突然连跳现象是游戏运行一段时间后蛇会突然加速连跳好几格或者暂停后恢复时蛇跳了一段。原因是setInterval在逻辑执行超时时会堆积回调。解决方法是改用递归setTimeout每步执行完再排下一步保证不会堆积。暂停时clearTimeout恢复时重新调用gameLoop。4.4 撞自己判定把尾巴算进去导致误杀现象是蛇贴着尾巴走时莫名死亡明明尾巴下一步就移走了。原因是撞自己判定检查了整条蛇包括即将移走的尾巴。解决方法是先判断这一步是否吃食物不吃的话检查时排除最后一节吃的话才检查整条蛇。这个边界在蛇长度较短时不容易发现蛇长了以后必现。4.5 手机上没有方向键玩不了现象是页面在手机上能打开但没法控制蛇。原因是只绑了键盘事件。解决方法是加触摸滑动检测记录touchstart和touchend的坐标差判断滑动方向映射到nextDirection。或者加四个屏幕按钮点击触发方向。滑动体验更好但要注意阻止页面滚动。5. 让贪吃蛇更耐玩难度曲线、存档与一个调试技巧基础版跑通后真正决定这个项目值不值得继续投入的是难度曲线和可扩展性。我一般会做三件事把速度调整从「每吃一个减 5 毫秒」改成按分数分段比如 0 到 50 分保持 150 毫秒50 到 100 分降到 120100 分以上降到 90这样前期不会太难后期有挑战。分段表放在一个数组里step里根据score查表设speed比线性递减更可控。第二件事是加本地存档。用localStorage存最高分页面加载时读出来显示在计分板上。贪吃蛇这种小游戏最高分是唯一的长期激励没有它玩家玩两把就关了。存的时候用localStorage.setItem(snake_high, score)读的时候Number(localStorage.getItem(snake_high)) || 0。注意localStorage在隐私模式下可能不可用包一层try/catch更稳。第三件事是一个调试技巧加一个「慢速模式」开关按住某个键时把speed临时调到 500 毫秒松开恢复。这样调试碰撞逻辑时能看清每一步不用改代码重启。实现上在keydown里判断按键改state.speedkeyup时恢复。这个技巧帮我省了很多次「蛇死得莫名其妙」的排查时间。难度分数区间每步间隔简单0-50150ms普通51-100120ms困难101-20090ms地狱20060ms这张表可以直接抄改数值就行。注意间隔不要低于 60 毫秒再快人眼和手指都跟不上体验反而差。最后说个习惯每次改完逻辑先用手动单步模式跑一遍边界情况——蛇长到填满棋盘、食物刷在角落、蛇头贴着尾巴走。这些场景自动化测试不好写但手动跑两分钟就能发现大部分 bug。贪吃蛇代码量不大但边界不少别等玩家反馈才去修。希望帮到你。本文还有配套的精品资源点击获取