
最近花了一个下午用原生 JavaScript 复刻了经典游戏《贪吃蛇》过程意外地解压也很适合拿来给前端新手当练手项目。写前端界面时我在花猫导航huamaodh.com的设计资源分类里找到了几个免费的图标和配色参考省去了自己折腾 UI 的时间。今天就把整个项目的思路和代码拆解出来从零开始带你实现一个能在浏览器里直接玩的贪吃蛇。一、游戏规则与设计思路经典贪吃蛇的核心逻辑很简单一条蛇在网格中移动初始有一个身体长度用键盘方向键控制蛇的移动方向地图上随机出现食物蛇吃到食物后身体变长得分增加蛇撞到墙壁或自己的身体游戏结束我们在这个基础上做一点微创新食物会有不同类型普通食物加分、金色食物额外加分同时移动速度随分数逐渐加快让游戏更有挑战性。技术栈纯 HTML CSS JavaScript不用任何框架。所有代码放在一个.html文件中双击就能运行。二、HTML 结构搭建页面上需要三个核心区域游戏画布、得分面板和控制按钮。html!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title 贪吃蛇/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #1a1a2e; display: flex; justify-content: center; align-items: center; min-height: 100vh; font-family: PingFang SC, Microsoft YaHei, sans-serif; } .game-wrapper { text-align: center; } .game-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; color: #eee; } .score-box { background: rgba(255,255,255,0.08); border-radius: 12px; padding: 10px 24px; } .score-label { font-size: 12px; color: #8892b0; letter-spacing: 1px; } .score-value { font-size: 32px; font-weight: bold; color: #64ffda; } .best-score { background: rgba(100,255,218,0.1); border: 1px solid rgba(100,255,218,0.3); border-radius: 12px; padding: 10px 20px; color: #64ffda; font-size: 14px; } canvas { display: block; margin: 0 auto; border-radius: 12px; background: #16213e; box-shadow: 0 0 30px rgba(100,255,218,0.1); } .game-controls { margin-top: 20px; display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; } .btn { background: rgba(255,255,255,0.08); color: #eee; border: 1px solid rgba(255,255,255,0.15); padding: 12px 28px; border-radius: 8px; font-size: 15px; cursor: pointer; transition: all 0.2s; letter-spacing: 1px; } .btn:hover { background: rgba(100,255,218,0.15); border-color: #64ffda; color: #64ffda; } .btn.primary { background: #64ffda; color: #1a1a2e; border: none; font-weight: 600; } .btn.primary:hover { background: #48c9b0; } .game-tip { color: #5a6a8a; font-size: 13px; margin-top: 16px; } /* 移动端虚拟方向键 */ .mobile-dpad { display: none; margin-top: 20px; } .dpad-row { display: flex; justify-content: center; gap: 10px; margin-bottom: 10px; } .dpad-btn { width: 60px; height: 60px; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15); border-radius: 12px; color: #eee; font-size: 24px; cursor: pointer; display: flex; align-items: center; justify-content: center; user-select: none; transition: background 0.15s; } .dpad-btn:active { background: rgba(100,255,218,0.25); } media (max-width: 600px) { .mobile-dpad { display: block; } } /style /head body div classgame-wrapper div classgame-header div classscore-box div classscore-label当前得分/div div classscore-value idscore0/div /div div classbest-score 最高分 span idbestScore0/span /div /div canvas idgameCanvas width400 height400/canvas div classgame-controls button classbtn primary idstartBtn开始游戏/button button classbtn idpauseBtn disabled暂停/button button classbtn idresetBtn重置/button /div !-- 移动端方向键 -- div classmobile-dpad div classdpad-row div classdpad-btn>// 游戏配置 const CONFIG { gridSize: 20, // 每格像素大小 cols: 20, // 列数 rows: 20, // 行数 baseSpeed: 150, // 初始移动间隔毫秒 minSpeed: 60, // 最快速度 speedStep: 5, // 每吃一个食物加速的毫秒数 initialLength: 3, // 蛇初始长度 }; // 游戏状态 let snake []; let food null; let goldenFood null; // 金色食物额外加分 let direction right; let nextDirection right; let score 0; let bestScore 0; let gameRunning false; let gamePaused false; let gameLoopId null; let speed CONFIG.baseSpeed; // DOM 元素 const canvas document.getElementById(gameCanvas); const ctx canvas.getContext(2d); const scoreEl document.getElementById(score); const bestScoreEl document.getElementById(bestScore); const startBtn document.getElementById(startBtn); const pauseBtn document.getElementById(pauseBtn); const resetBtn document.getElementById(resetBtn); // 读取本地最高分 bestScore parseInt(localStorage.getItem(snakeBestScore) || 0); bestScoreEl.textContent bestScore; // 初始化游戏 function initGame() { // 蛇初始在中间水平排列 const startX Math.floor(CONFIG.cols / 2); const startY Math.floor(CONFIG.rows / 2); snake []; for (let i 0; i CONFIG.initialLength; i) { snake.push({ x: startX - i, y: startY }); } direction right; nextDirection right; score 0; speed CONFIG.baseSpeed; food null; goldenFood null; scoreEl.textContent 0; generateFood(); generateGoldenFood(); } // 食物生成 function getRandomGridPos() { return { x: Math.floor(Math.random() * CONFIG.cols), y: Math.floor(Math.random() * CONFIG.rows), }; } // 检查一个位置是否被蛇占据 function isPositionOnSnake(pos) { return snake.some(segment segment.x pos.x segment.y pos.y); } // 生成普通食物 function generateFood() { if (food) return; // 已有食物时不重复生成 let pos; do { pos getRandomGridPos(); } while (isPositionOnSnake(pos) || (goldenFood goldenFood.x pos.x goldenFood.y pos.y)); food pos; } // 生成金色食物概率 10% function generateGoldenFood() { if (goldenFood) return; if (Math.random() 0.1) return; // 10% 概率出现 let pos; do { pos getRandomGridPos(); } while (isPositionOnSnake(pos) || (food food.x pos.x food.y pos.y)); goldenFood pos; } // 游戏主循环 function gameLoop() { if (!gameRunning || gamePaused) return; // 更新方向 direction nextDirection; // 计算新蛇头位置 const head snake[0]; let newHead { ...head }; switch (direction) { case up: newHead.y--; break; case down: newHead.y; break; case left: newHead.x--; break; case right: newHead.x; break; } // 检查是否撞墙 if (newHead.x 0 || newHead.x CONFIG.cols || newHead.y 0 || newHead.y CONFIG.rows) { endGame(); return; } // 检查是否撞到自己忽略尾部因为尾部会移走除非吃到食物 // 先假设不吃食物尾部会移除所以检查时排除最后一个节点 const willEat (food newHead.x food.x newHead.y food.y) || (goldenFood newHead.x goldenFood.x newHead.y goldenFood.y); const checkBody willEat ? snake : snake.slice(0, -1); if (checkBody.some(segment segment.x newHead.x segment.y newHead.y)) { endGame(); return; } // 添加新头 snake.unshift(newHead); // 检查是否吃到食物 if (food newHead.x food.x newHead.y food.y) { score 10; food null; generateFood(); generateGoldenFood(); // 加速 speed Math.max(CONFIG.minSpeed, speed - CONFIG.speedStep); } else if (goldenFood newHead.x goldenFood.x newHead.y goldenFood.y) { score 30; goldenFood null; generateGoldenFood(); speed Math.max(CONFIG.minSpeed, speed - CONFIG.speedStep * 2); } else { // 没吃到食物移除尾 snake.pop(); } scoreEl.textContent score; // 绘制 draw(); // 重新设置定时器以应用新速度 clearTimeout(gameLoopId); gameLoopId setTimeout(gameLoop, speed); } // 绘制 function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制网格线浅暗线 ctx.strokeStyle #2a3a5a; ctx.lineWidth 0.5; for (let i 0; i CONFIG.cols; i) { ctx.beginPath(); ctx.moveTo(i * CONFIG.gridSize, 0); ctx.lineTo(i * CONFIG.gridSize, canvas.height); ctx.stroke(); } for (let j 0; j CONFIG.rows; j) { ctx.beginPath(); ctx.moveTo(0, j * CONFIG.gridSize); ctx.lineTo(canvas.width, j * CONFIG.gridSize); ctx.stroke(); } // 绘制食物 if (food) { ctx.fillStyle #ff6b6b; ctx.beginPath(); const fx food.x * CONFIG.gridSize CONFIG.gridSize / 2; const fy food.y * CONFIG.gridSize CONFIG.gridSize / 2; ctx.arc(fx, fy, CONFIG.gridSize / 2 - 2, 0, Math.PI * 2); ctx.fill(); } // 绘制金色食物 if (goldenFood) { ctx.fillStyle #ffd700; ctx.shadowColor #ffd700; ctx.shadowBlur 8; ctx.beginPath(); const gx goldenFood.x * CONFIG.gridSize CONFIG.gridSize / 2; const gy goldenFood.y * CONFIG.gridSize CONFIG.gridSize / 2; ctx.arc(gx, gy, CONFIG.gridSize / 2 - 1, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur 0; } // 绘制蛇 snake.forEach((segment, index) { const x segment.x * CONFIG.gridSize; const y segment.y * CONFIG.gridSize; const size CONFIG.gridSize - 1; // 蛇头颜色稍亮 if (index 0) { ctx.fillStyle #64ffda; ctx.shadowColor #64ffda; ctx.shadowBlur 6; } else { // 身体渐变 const alpha 1 - (index / snake.length) * 0.5; ctx.fillStyle rgba(72, 201, 176, ${alpha}); ctx.shadowBlur 0; } ctx.beginPath(); ctx.roundRect(x 0.5, y 0.5, size, size, 4); ctx.fill(); }); ctx.shadowBlur 0; } // 游戏控制 function startGame() { if (gameRunning !gamePaused) return; if (!gameRunning) { initGame(); gameRunning true; gamePaused false; startBtn.textContent 重新开始; pauseBtn.disabled false; pauseBtn.textContent 暂停; draw(); gameLoopId setTimeout(gameLoop, speed); } } function togglePause() { if (!gameRunning) return; gamePaused !gamePaused; pauseBtn.textContent gamePaused ? 继续 : 暂停; if (!gamePaused) { gameLoopId setTimeout(gameLoop, speed); } else { clearTimeout(gameLoopId); } } function endGame() { gameRunning false; clearTimeout(gameLoopId); startBtn.textContent 再来一局; pauseBtn.disabled true; pauseBtn.textContent 暂停; // 更新最高分 if (score bestScore) { bestScore score; bestScoreEl.textContent bestScore; localStorage.setItem(snakeBestScore, bestScore); } // 绘制结束画面 ctx.fillStyle rgba(0,0,0,0.6); ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle #ff6b6b; ctx.font bold 24px PingFang SC; ctx.textAlign center; ctx.fillText(游戏结束, canvas.width/2, canvas.height/2 - 10); ctx.fillStyle #eee; ctx.font 14px PingFang SC; ctx.fillText(得分: ${score}, canvas.width/2, canvas.height/2 30); ctx.textAlign start; } function resetGame() { clearTimeout(gameLoopId); gameRunning false; gamePaused false; startBtn.textContent 开始游戏; pauseBtn.disabled true; pauseBtn.textContent 暂停; initGame(); draw(); } // 键盘控制 document.addEventListener(keydown, (e) { if (!gameRunning || gamePaused) return; const key e.key.toLowerCase(); const oppositeMap { arrowup: down, arrowdown: up, arrowleft: right, arrowright: left, w: down, s: up, a: right, d: left }; let newDir null; if (key arrowup || key w) newDir up; else if (key arrowdown || key s) newDir down; else if (key arrowleft || key a) newDir left; else if (key arrowright || key d) newDir right; if (newDir) { // 禁止原地掉头 if (oppositeMap[key] ! direction || snake.length 1) { nextDirection newDir; } e.preventDefault(); } // 空格键暂停 if (key ) { togglePause(); e.preventDefault(); } }); // 移动端方向键 document.querySelectorAll(.dpad-btn).forEach(btn { btn.addEventListener(click, () { if (!gameRunning || gamePaused) return; const dir btn.dataset.dir; const opposite { up: down, down: up, left: right, right: left }; if (opposite[dir] ! direction || snake.length 1) { nextDirection dir; } }); }); // 按钮事件 startBtn.addEventListener(click, startGame); pauseBtn.addEventListener(click, togglePause); resetBtn.addEventListener(click, resetGame); // 初始化绘制 initGame(); draw();把上面的 HTML 和 JavaScript 合并保存为snake.html用浏览器打开一个完整的贪吃蛇游戏就跑起来了。四、代码解读与设计亮点虽然代码只有 200 多行但里面包含了几个值得留意的小设计1. 方向锁定防止原地掉头玩家按了反方向键时我们不会立即执行而是判断新方向是否与当前方向相反。只有蛇身长度大于 1 时才阻止这样蛇只有一个头时可以自由转向——避免一开始就卡死。2. 食物系统普通食物 金色食物除了红色圆形食物10 分地图上会低概率出现闪烁的金色食物30 分并且额外加速。这种随机奖励机制给游戏增加了策略性是稳扎稳打吃普通食物还是冒险去追金豆3. 移动端适配用 CSS 媒体查询隐藏了一套虚拟方向键在手机屏幕上自动显示。触屏用户也能愉快玩耍不需要外接键盘。4. 最高分本地存储利用localStorage把最高分存下来刷新页面也不会丢失。这是一个非常实用的前端技能几乎所有小游戏都会用到。五、界面美化与灵感来源游戏的功能逻辑写完后视觉呈现同样重要。背景的深蓝配色、霓虹青色的蛇身以及网格线的微弱存在感都是为了让游戏区看起来干净、有科技感。在做这个项目的 UI 设计时我习惯先找一个成熟产品的风格作为参考。在它的设计资源分类下翻了几组配色方案和图标集很快就确定了主色调和按钮样式效率比从零自己调高很多。如果你后续想进一步美化游戏界面比如自定义食物图标、蛇头皮肤收录的资源站也能帮你快速找到合适的素材。六、扩展方向基础版贪吃蛇已经可以玩得很开心了如果想继续深挖可以考虑这些方向关卡系统设计不同的地图障碍物蛇碰到障碍也判定死亡。道具系统随机出现加速鞋、减速钟、护盾等道具增加变数。双人对战同屏两条蛇各自用不同键位控制互相围堵。后端排行榜把分数上传到服务器做一个全球排行榜社交属性拉满。这些扩展涉及的算法思路——碰撞检测、状态管理、网络同步——实际上和游戏开发甚至推荐系统中的很多问题都是相通的作为算法工程师的业余练手再合适不过。七、总结从零实现一个贪吃蛇代码量不大但涵盖了 Canvas 绘图、游戏循环、键盘事件、状态机和数据持久化等前端核心知识。对于正在学习 JavaScript 的同学来说这是一个比 Todo List 更有趣的实战项目。做完之后你可以在上面随意添砖加瓦把它变成自己专属的摸鱼神器。希望这篇教程对你有帮助。现在就打开编辑器花半小时敲一遍然后叫上同事比一比谁的分数更高。在代码的间隙里偷来一点简单的快乐也是一种值得坚持的摸鱼哲学。