从 0 到可玩的贪吃蛇:3 个核心对象 + 1 个 setTimeout 循环搞定事件驱动 JavaScript 📅 发布时间:2026/9/19 23:27:10 👁 浏览次数: 从 0 到可玩的贪吃蛇3 个核心对象 1 个 setTimeout 循环搞定事件驱动 JavaScript【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculumcurriculum 仓库 Web 开发 101 课程「Events Effects」项目 project_events_effects.md 能用纯 JavaScript 做出一个响应键盘方向键、越吃越长的贪吃蛇游戏不靠框架、不靠 canvas只有一个键盘、一个定时器和一堆 div。场景想在页面里放一个真的能玩的小游戏学完 DOM 和事件之后你已经能让页面响应点击了。但点击是被动的你真正想练的是那种自己会动的页面——比如打开一个生日页面朋友可以直接上手玩一局贪吃蛇。贪吃蛇正好卡在这个能力点上输入是键盘方向键心跳是定时器输出是网格事件编程的三大件全在里面而且体量小到晚上就能写完。项目的完整任务定义和大量历史学员解法都在仓库的 project_events_effects.md 里做完之后可以拿自己的建模方式和别人的对比。原理先行贪吃蛇到底是怎么跑起来的动手前先把数据流的闭环在脑子里过一遍整个项目只有四个角色状态建模游戏世界 棋盘 grid 蛇 snake 食物 food 三个对象。蛇不是一个点而是坐标对数组第一个是头、最后一个是尾循环驱动游戏循环按固定间隔推进回合每个回合 移动一格 → 判定进食与碰撞 → 调度下一回合由setTimeout驱动交互输入document上的keydown监听器负责改蛇的方向循环在下一回合读取新方向渲染输出render()把当前状态重新铺成 40×40 的div格子。四个角色互不越权输入只改方向、循环只推状态、渲染只读状态。屏幕永远只是状态的投影这句话是本项目的总纲——后面每个坑都是违反了它才出现的。第一步先建模3 个对象撑起整个游戏为什么建模放最前面因为移动、进食、碰撞检测的所有逻辑都建立在怎么读写这几个对象上模型定错后面全得返工。// 40×40 棋盘每个格子初始是一个空格 var grid []; for (var row 0; row 40; row) { grid[row] []; for (var col 0; col 40; col) { grid[row][col] ; } } // 蛇方向 全身坐标对数组第一个元素是蛇头 var snake { position: [20, 20], direction: r, // 初始方向右 body: [[20, 20]] }; // 食物随时可重新随机的一组坐标 var food { position: [10, 10] };关键设计只有一条蛇是坐标对数组不是单个坐标。移动 头部插入新坐标变长一格 这回合不弹出尾部仅此而已。想通这一点吃食物长身体的逻辑两行就能写完。grid 在这里当棋盘账本负责回答这个格子现在有什么真正的位置权威在snake.body和food.position手里。最易踩的坑坐标对顺序要统一成[row, col]并且在move()、render()、碰撞检测里保持一致。一旦混用蛇会走到错误的行上去而且屏幕看起来没坏极难排查。验证动作建完对象后在控制台执行console.log(snake.body[0], snake.direction)应打印[20,20] r。第二步 ⏱循环驱动——游戏循环为什么用 setTimeout循环是游戏的心跳职责是每隔一拍走一步然后决定要不要继续活着。为什么不用 for 循环因为游戏不能冻住页面还必须随时可停for 会一口气跑到底中途既读不到玩家的方向变化也没法在撞墙那一刻刹车。setTimeout只调度一次每回合结束时你都能读到最新状态再决定是否继续。function turn() { move(); // 向前推进一格 render(); // 重画画面 var head snake.body[0]; if (isOutOfBoard(head) || hitsItself(head)) { return alert(Game over); // 不再调度循环自然终止 } setTimeout(turn, 150); // 调度下一回合 } function move() { var next snake.body[0].slice(); // 复制蛇头别动原坐标对 if (snake.direction u) next[0]--; else if (snake.direction d) next[0]; else if (snake.direction l) next[1]--; else next[1]; snake.body.unshift(next); if (!ateFood(next)) snake.body.pop(); // 没吃到才弹尾 }ateFood(next)的判定是新蛇头与food.position重合则返回true并顺手把食物随机挪到棋盘另一处Math.floor(Math.random() * 40)生成行和列。isOutOfBoard判断蛇头是否越出 0~39 边界hitsItself判断新蛇头是否已存在于蛇身中尾部除外见下方踩坑表。move()里藏着两个决定成败的细节一是slice()复制蛇头——直接改旧头部数组的话身体里新旧坐标会共享同一个引用画面会撕裂二是变长 不弹尾不需要任何长度 1的额外逻辑。验证动作先注释掉碰撞判定两行再启动游戏蛇应该笔直走到底后消失。如果它原地抖动或者身体断成两截查unshift/pop的顺序。第三步键盘监听怎么写——方向键转向的完整写法输入是最简单的部分却藏着最多的坑。把keydown绑在document上而不是某个具体元素利用事件冒泡无论焦点落在页面哪块区域按键事件都会冒泡到document被接住你不用关心玩家站在哪个格子上。document.addEventListener(keydown, function (event) { var map { ArrowUp: u, ArrowDown: d, ArrowLeft: l, ArrowRight: r }; if (!map[event.key]) return; if (map[event.key] oppositeOf(snake.direction)) return; // 拦掉 180° 掉头 event.preventDefault(); // 阻止方向键滚动页面 snake.direction map[event.key]; console.log(新方向:, snake.direction); });传给addEventListener的函数是一个回调函数你把函数扔向未来按键真正发生的那一刻由浏览器调它并递给你一个event对象里面装着这次按键的全部信息event.key就是按键名。回调机制的完整拆解可以看仓库里的 event_driven.md。最易踩的坑忘了event.preventDefault()每按一次方向键页面就上下滚一截游戏看起来像抽风——其实游戏没坏是页面在动。验证动作先不启动循环只按方向键控制台应逐个打印新方向值如果按oppositeOf拦掉头后打印r u l d说明输入层完全就位。脚本记得放在/body前加载顺序参考 js_to_browser.md。第四步渲染输出——把网格变成一堆 divrender()是全游戏唯一碰屏幕的地方。它的策略是每回合推倒重画而不是去修补个别格子项目体量足够小允许用暴力但清晰的方式换来状态与画面不同步这类 bug 基本不可能发生。DOM 创建与追加的手法createElement、appendChild详见 DOM_manipulation_and_events.md。function render() { // 1. 重建棋盘账本全部清空为空格再标蛇身与食物 for (var r 0; r 40; r) for (var c 0; c 40; c) grid[r][c] ; snake.body.forEach(function (seg) { grid[seg[0]][seg[1]] S; }); grid[food.position[0]][food.position[1]] F; // 2. 把账本铺成 div先清空旧节点否则每回合格子会叠加 var board document.getElementById(grid); board.innerHTML ; // …双循环建 1600 个 divS 加 snake 类F 加 food 类 }样式只需要三行.cell { width: 20px; height: 20px; border: 1px solid #ccc; box-sizing: border-box; } .snake { background: green; } .food { background: red; }最易踩的坑格子默认是content-box加边框会增大实际占位尺寸。不加box-sizing: border-box棋盘每行会比上一行宽一圈网格直接错位盒模型原理见 the_box_model.md。验证动作跑一次render()页面应出现一个规整的 40×40 方格棋盘和一个绿色格子。如果每行第 40 格都换行顶出去百分之百是尺寸问题不用回头查 JS。 踩坑清单调试贪吃蛇的 5 个高频问题现象原因解法棋盘每行错位换行content-box下格子加边框后尺寸变大统一加box-sizing: border-box按方向键页面跟着滚动方向键默认行为是滚动页面keydown回调里event.preventDefault()蛇尾不跟随、棋盘留残影未弹尾或坐标对引用被新旧两格共享移动 头插新坐标 没吃到食物就弹尾蛇一掉头就撞死自己向右时按左蛇头直接贴上第二节身体按键回调里拦截与当前方向相反的方向蛇头追上尾巴时误判游戏结束尾巴本回合即将被弹出蛇头进入其位置合法hitsItself遍历时跳过最后一段进阶方向计分让蛇越长每份食物分值越高score snake.body.length把对象状态和游戏数值挂上钩。加速每吃一个食物把延迟减一点Math.max(50, speed - 5)setTimeout天然适配——下一回合只在当前回合结束时才调度每次都读最新值无需重建定时器。图片蛇身把字符格子换成真实图片顺带练img元素的动态插入与样式控制。收尾贪吃蛇项目的价值不在蛇会动而在于你第一次把事件驱动的完整心智模型跑通了输入只改方向、循环只推状态、渲染只投影状态。这个分层想清楚后仓库 JavaScript 线的下一个作业 project_ajax_forms.md 和 project_apis.md 就可以顺势接上——把事件思维延伸到异步请求与数据交互里去。【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考