ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

网页版贪吃蛇源码拆解:从DOM到状态机的前端小游戏实践

网页版贪吃蛇源码拆解:从DOM到状态机的前端小游戏实践 简介这套基于HTML、CSS与JavaScript实现的网页版贪吃蛇游戏源码面向前端初学者、Web游戏开发爱好者及想快速体验经典游戏实现的读者。压缩包共8个文件含1个HTML页面、2个CSS样式表、1个JavaScript逻辑脚本与4个方向控制图标整体约653KB结构一目了然浏览器打开即可直接游玩。目前已有178人学习借鉴对入门项目而言具有一定参考价值。通过阅读代码可以清晰掌握键盘事件监听、蛇身移动与增长、食物随机生成、碰撞检测、得分刷新等核心逻辑同时代码分层简洁方便在此基础上扩展关卡难度、音效或移动端触控支持是一份轻量且完整的原生JavaScript游戏开发示例。1. 网页版贪吃蛇不是玩具是前端状态机的最小样本网页版贪吃蛇听起来是一个已经被写烂的练手项目但把 index.html、snake.js、base.css、snake.css 以及 down.png、left.png、up.png、right.png 这八个文件从 rar 包里解出来逐行过一遍你会发现它覆盖了一个前端小游戏从页面结构到交互逻辑的所有环节。和 C 语言控制台版贪吃蛇那种“循环读按键 清屏重绘”的模型不同网页版天然是事件驱动DOM 负责展示JS 负责状态迁移两张 CSS 负责界面表现。这套资源适合两类人。一类是刚学完 JavaScript DOM 操作想找一个完整项目练手的前端入门者另一类是需要在短时间内搭一个 H5 小游戏原型的工程师。整个游戏没有后端依赖解压后直接用浏览器打开 index.html 就能玩操作就是 W、A、S、D 四个键吃食物加分撞墙或撞到自己结束。我更想强调的是后面的东西贪吃蛇的代码量不大但状态不少蛇身坐标、当前方向、下一帧方向、食物位置、得分、游戏是否结束每一项都清清楚楚。把这几个状态之间的关系理清楚再去做俄罗斯方块、扫雷或者迷宫生成会发现思路几乎可以平移。这也是这份源码值得拆的原因。2. 从 index.html 到 CSS把游戏界面拆成可维护的模块2.1 解压 rar 后先确认文件清单拿到网页版贪吃蛇游戏.rar以后不要急着双击打开先解压确认目录结构。Linux 或 macOS 下常见做法是用 unrarunrar x 网页版贪吃蛇游戏.rar ls -l没有 unrar 就先用 WinRAR 或 7-Zip 解压效果一样。解压后应该能看到八个文件index.html、snake.js、base.css、snake.css以及down.png、left.png、up.png、right.png四张方向箭头图片。看到这个文件列表基本上能判断出两件事一是这个项目没有依赖任何 CDN 或前端框架离线状态下一样能跑二是作者把方向控制设计成了键盘和图片按钮两套入口后者是为触屏设备预留的。文件命名也很有规律HTML、JS、CSS 按职责分开图片按方向语义命名没有混淆。坏习惯是把所有样式写在一个 style 标签里、把所有逻辑塞进一个 script 标签这个项目没有这个问题后面改起来会省很多事。2.2 index.htmlDOM 层级与脚本加载顺序index.html是游戏的骨架。按同类源码的常见结构大致是这样的!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 link relstylesheet hrefbase.css link relstylesheet hrefsnake.css /head body div idgame-wrapper div idscore-board 得分span idscore-value0/span /div canvas idgame-canvas width400 height400/canvas div iddirection-panel img srcleft.png altleft img srcup.png altup img srcright.png altright img srcdown.png altdown /div /div script srcsnake.js/script /body /html这段标记里有两个容易忽略的细节。第一两个 CSS 文件放在 head 中snake.js 放在 body 末尾。这样脚本执行时canvas、得分板和方向按钮已经存在于 DOM 中可以直接绑定事件。如果因为习惯把 script 塞进 head运行时经常报Cannot read properties of null原因就是节点还没解析出来。第二canvas 的 width 和 height 是画布物理尺寸不是 CSS 显示尺寸。后期如果做响应式需要按 canvas 宽高和 GRID_SIZE 的倍数关系缩放不能直接改 width 属性。方向面板里的四张 img 对应资源包里的四张 PNG。它们的存在意味着即使不接键盘通过点击或触摸图片也能指定方向。这个入口在源码里可能只是静态摆着但已经给移动端适配留下了接口第 4 章会把它接上。2.3 base.css 和 snake.css 的分层思路两个样式文件的分工从命名就能看出来。base.css 管通用基础样式snake.css 管游戏专属主题。常见分工如下表文件职责典型内容base.css全局基础样式盒模型重置、页面背景、字体、居中布局snake.css游戏主题样式canvas 边框与圆角、得分板排版、方向按钮布局、响应式适配这种拆分的好处是职责边界清楚。想换一套站点皮肤时只动 base.css想调整游戏配色和按钮尺寸时只动 snake.css互不影响。单人小项目当然可以合成一个 style.css但拆开写更符合多人协作时样式隔离的习惯也方便将来把游戏组件单独抽出去。方向按钮这里有个值得注意的点。四张图片要组成方向键常见做法是 2x2 网格或十字布局。用 CSS Grid 或 Flexbox 都能实现关键是要给 image 设置足够大的点击区域并在 hover 或 active 状态下给一点视觉反馈不然玩家按下去不知道有没有生效。另外移动端要记得在按钮上关闭默认的触摸高亮和双击缩放行为后面 4.1 会给出具体写法。2.4 直接打开与放到网站根目录的差异很多教程说“双击 index.html 就能运行”对这个项目来说基本是对的。它没有跨域请求JS 和图片都是相对路径file:// 协议下能正常工作。但如果后续要加 localStorage 保存最高分或者要让同一局域网里的手机访问我一般会先在项目目录下起一个静态文件服务python3 -m http.server 8080然后通过http://localhost:8080访问。这样文件协议带来的本地存储限制、浏览器安全策略差异都会消失模拟出来的环境更接近真实部署。起完服务以后手机和电脑连同一个网络再用电脑 IP 加端口访问就能直接调试触摸方向按钮。提示解压后发现方向按钮图片不显示先检查文件名大小写。资源里是 down.png、left.png、up.png、right.pngHTML 引用路径必须和实际文件完全一致。3. snake.js移动、碰撞、得分与 setInterval 的取舍3.1 用 state 对象管理蛇、方向和得分snake.js 是整套源码里信息密度最高的文件。贪吃蛇的逻辑不复杂但直接把蛇身数组、方向变量、得分到处写全局很容易在加功能时改乱。我一般会用一个 state 对象把游戏状态集中起来const state { snake: [ { x: 10, y: 10 }, // 蛇头 { x: 9, y: 10 }, { x: 8, y: 10 } ], direction: right, // 当前生效方向 nextDirection: right, // 玩家最新输入方向 food: null, score: 0, status: ready // ready | running | over };数组里第 0 项是蛇头后面是身体。direction 和 nextDirection 两个字段是分开的这个设计直接决定了反向保护怎么写3.3 会细说。status 字段将游戏区分为未开始、运行中、结束三种状态按键监听和渲染函数都要先判断它否则游戏结束后按方向键还会继续移动。这种写法的意义在于数据与渲染分离。moveSnake、checkCollision 只改 staterender 只读 state两边不互相插队。后面要加暂停、重开、最高分全部围绕 state 做文章代码不会越改越乱。3.2 主循环为什么是 setInterval 而不是 requestAnimationFrame刚接触前端动画的人容易直接上 requestAnimationFrame结果发现蛇跑得飞快。这是因为 rAF 是跟着显示器刷新率走的每秒 60 次而贪吃蛇需要的是一格一格离散移动。用固定间隔的定时器更合适const SPEED 200; // 每 200ms 走一格每秒 5 格 let timer null; function start() { if (state.status running) return; state.status running; timer setInterval(tick, SPEED); } function tick() { moveSnake(); checkCollision(); render(); }SPEED 是核心节奏参数。200ms 对新手刚好改成 120ms 就是明显加速的困难模式。setInterval 的缺点是间隔在创建时固定想要动态变化必须先 clearInterval 再重新创建很容易把 timer 状态搞乱。所以第 4.3 做难度曲线时我会改用 setTimeout 链。render 做的事情很单一清空画布遍历 state.snake 坐标画方块再在 state.food 位置画食物。不要在 render 里处理移动和碰撞那会让逻辑和绘制耦合出现“明明没按方向键蛇却动了”这类很难查的 bug。3.3 WASD 键盘控制与方向锁键盘是这个游戏的主输入WASD 和方向键都要支持。比较稳妥的监听写法是document.addEventListener(keydown, (e) { const map { KeyW: up, KeyS: down, KeyA: left, KeyD: right, ArrowUp: up, ArrowDown: down, ArrowLeft: left, ArrowRight: right }; const dir map[e.code]; if (dir) { changeDirection(dir); e.preventDefault(); // 阻止方向键滚动页面 } }); function changeDirection(dir) { if (state.status ! running) return; state.nextDirection dir; }这里用 e.code 而不是 keyCode。e.code 代表物理按键不会因为浏览器语言或输入法状态变成中文后失效keyCode 已经被标记废弃在兼容性上也有坑。真正的方向锁在 changeDirection 里只做了一半另一半在 tick 的移动阶段。原因是玩家可能会在一个 tick 间隔内快速按两次键比如蛇向右走时先按 W 再按 A。如果按一次就立刻把 direction 改成上再按一次又改成左第二次方向会被直接采纳蛇在下一个 tick 里就会向左走相当于向右的蛇瞬间掉头穿进自己的身体。正确的处理是用 nextDirection 先暂存输入移动时只接受与当前方向不相反的最新方向const opposite { up: down, down: up, left: right, right: left }; if (dir ! opposite[state.direction]) { state.direction dir; }这样每个 tick 最多转向一次并且永远不会 180 度掉头这是经典贪吃蛇手感的关键。3.4 食物生成与碰撞检测食物的位置不能和蛇身重合否则会重叠显示且吃不到。生成逻辑常见做法是随机选坐标如果落在蛇身上就重选function spawnFood() { let pos; do { pos { x: Math.floor(Math.random() * GRID_SIZE), y: Math.floor(Math.random() * GRID_SIZE) }; } while (state.snake.some(seg seg.x pos.x seg.y pos.y)); state.food pos; }do...while 在蛇还短的时候没有问题但理论上蛇会越吃越长如果占满整个画布这个循环就退化成死循环。严谨的写法是先统计空位数量为空时直接判定胜利。这个边界虽然很难触发但值得在代码里留一个防御分支。移动和吃食物是同一段逻辑function moveSnake() { const head state.snake[0]; const newHead { ...head }; switch (state.direction) { case up: newHead.y - 1; break; case down: newHead.y 1; break; case left: newHead.x - 1; break; case right: newHead.x 1; break; } state.snake.unshift(newHead); if (state.food newHead.x state.food.x newHead.y state.food.y) { state.score 10; spawnFood(); } else { state.snake.pop(); } }unshift 把新蛇头插到数组头部pop 把尾巴移除身体就自然向前滑动了一格。吃到食物时不 pop蛇身长度加 1这就是“变长”的本质。要注意先 unshift 再 pop顺序反过来会导致蛇永远差一格不移动。碰撞检测分为两部分墙和身体function checkCollision() { const head state.snake[0]; if (head.x 0 || head.x GRID_SIZE || head.y 0 || head.y GRID_SIZE) { gameOver(); return; } for (let i 1; i state.snake.length; i) { if (state.snake[i].x head.x state.snake[i].y head.y) { gameOver(); return; } } }为什么从索引 1 开始遍历蛇头自己和自己同坐标不算碰撞所以要从第二节身体开始比较。反过来如果从 0 开始游戏一运行就会判定失败。3.5 速度与网格参数改哪里能直接改变难度源码里影响难度和显示的核心参数通常集中在文件顶部。常用默认值如下参数建议值作用GRID_SIZE20画布 400×400 时被分为 20×20 个格子SPEED200ms每次移动的间隔越小速度越快INITIAL_LENGTH3初始蛇身长度影响开局压力SCORE_STEP10每吃一个食物增加的分数如果 GRID_SIZE 改成 30格子变小蛇的活动精度提高游戏会简单一些SPEED 调到 120ms整体节奏立刻紧张起来。调整时要注意所有坐标计算都必须依赖同一份 GRID_SIZE不能某处写 20、某处写 16。更工程化的做法是把这些常量放进一个 CONFIG 对象关于这个改法放在第 5.4 里具体说。4. 扩展实战触屏方向键、localStorage 最高分和难度曲线4.1 让四张方向图片真正可用资源包里的 left.png、up.png、right.png、down.png 不是装饰。它们在 index.html 里被放进了 direction-panel说明原作者预留了鼠标或触摸方向控制。纯 WASD 在手机上没法操作把这四张图接上事件是移动端的第一步。常见做法是给每张图片绑定 pointerdown 事件document.querySelectorAll(#direction-panel img).forEach(img { img.addEventListener(pointerdown, (e) { e.preventDefault(); const dir img.alt.toLowerCase(); // left/up/right/down changeDirection(dir); }); });用 alt 做映射比较简单但更严谨的方案是在 HTML 里加>const HIGH_SCORE_KEY snake_high_score; function gameOver() { state.status over; const high Number(localStorage.getItem(HIGH_SCORE_KEY) || 0); if (state.score high) { localStorage.setItem(HIGH_SCORE_KEY, String(state.score)); document.querySelector(#best-score).textContent state.score; } }这段代码有两个注意点。第一localStorage 存出来的值是字符串用 Number() 包一层再做比较否则30 100的结果会不符合预期。第二显示最高分的位置要在 index.html 里预留一个节点比如span idbest-score0/span放在得分板旁边。如果直接在 file:// 协议下双击运行个别浏览器会把 localStorage 禁掉控制台抛 SecurityError。放到第 2.4 的本地 HTTP 服务下就没有这个问题。这也解释了为什么“双击能跑”和“适合正式使用”是两件事。4.3 动态难度用 setTimeout 代替 setInterval固定 200ms 的节奏玩到后面会无聊。把主循环改成 setTimeout 链就能让蛇随分数逐渐加速let speed 200; function loop() { if (state.status ! running) return; moveSnake(); checkCollision(); render(); speed Math.max(80, 200 - state.score * 2); // 分数越高间隔越短 timer setTimeout(loop, speed); } function start() { if (state.status running) return; state.status running; loop(); }Math.max(80, ...) 是下限保护防止蛇快到完全无法操作。200 - score * 2 是线性变快得 10 分时加快 20ms得 50 分时加快 100ms曲线比较温和。想要更刺激可以改成分段函数if (state.score 100) speed 90; else if (state.score 50) speed 110; else speed 200 - state.score * 2;不同分段的节奏差异可以参考这张表分数区间间隔体验0 - 50200ms 递减到 100ms前期平稳新手能适应50 - 100110ms 左右开始有压迫感100 以上90ms接近操作上限主要考验预判改成 setTimeout 后有一个容易漏的点游戏结束时必须 clearTimeout(timer)。setInterval 时代也要求 clearInterval但不同点是 setTimeout 链每次执行都会生成新 timer如果 gameOver 里忘了清checkCollision 已经改了 statusloop 下一次进来会因为状态判断而退出看起来没出问题实际上多跑了一次没有意义的 tick。提前清掉更干净。4.4 和 C 语言控制台版贪吃蛇理解对比搜“贪吃蛇c语言程序代码”会看到大量控制台版本用二维数组表示地图用 getch() 阻塞读取键盘用 system(cls) 清屏重绘。这套网页版和它的本质差别是输入和渲染模型不同。C 语言版是循环里读键盘、刷新终端网页版是事件回调里改 state、定时器里改逻辑。如果把 C 语言版移植到浏览器要做的映射是把 while 换成 setInterval 回调把 getch() 换成 keydown 监听把 printf 换成 canvas 的 fillRect。数据结构是蛇身数组和食物坐标几乎可以原样搬过来。理解了这层对应关系看任何语言的小游戏都能快速定位思路输入怎么进入系统、状态怎么迁移、画面怎么刷新。这套网页版比控制台版多出来的 CSS 分层和事件机制恰恰是前端环境里最有价值的部分。5. 收尾调试技巧优先看控制台、路径大小写和渲染配置5.1 先开 DevTools后动代码网页版游戏最容易出的问题不是算法而是脚本压根没执行。双击 index.html 后画面空白先按 F12 打开开发者工具切到 Console。看到Uncaught TypeError: Cannot set properties of null基本就是 snake.js 在查找某个节点时index.html 里没有对应的 id。对照文件里的 id 逐字检查尤其注意大小写。Network 面板用来确认四张 PNG 是否全部 200 返回图片 404 时方向按钮区域会出现明显的占位框。5.2 路径大小写和编码HTML 里引用的 snake.js、snake.css 必须和解压出的文件名完全一致。Windows 文件系统默认不区分大小写问题不明显一旦部署到 Linux 服务器Snake.js 和 snake.js 就是两个文件。另一个隐蔽问题是 index.html 缺少meta charsetUTF-8中文注释和得分文字会乱码看起来像字符集问题实际和游戏逻辑无关。5.3 一份可抄的验证清单改代码前先按这张表过一遍能快速确认基础行为没被破坏检查项操作预期结果页面加载打开 index.html 并刷新画布与方向按钮正常控制台无报错基本移动按 D 键蛇头向右移动身体跟随约 5 格/秒反向保护蛇向右时按 A 键再按 W 键蛇不会直接掉头最多转向上方吃食物让蛇头碰到食物分数增加蛇身长度加 1新食物生成撞墙结束持续向墙移动游戏结束方向键不再影响蛇这五项分别覆盖初始化、输入、方向锁、增长、碰撞五条主要代码路径。哪一条不对就顺着 snake.js 里对应的 moveSnake、checkCollision 或 keydown 监听去查。5.4 把参数收敛到 CONFIG 对象最后一个我常用的收口改法是把散落的常量收进一个 CONFIG 对象const CONFIG { canvasSize: 400, gridSize: 20, initSpeed: 200, minSpeed: 80, initLength: 3, scoreStep: 10 };移动和渲染函数里全部用 CONFIG.gridSize、CONFIG.canvasSize 计算不再写死 20 或 400。以后再开困难模式只要在代码里定义一组 CONFIG_HARD覆盖 initSpeed 和 initLength 后传给 start移动和碰撞函数一行都不用改。本文还有配套的精品资源点击获取
返回列表