用HTML5 Canvas从零实现贪吃蛇小游戏(附完整代码)

用HTML5 Canvas从零实现贪吃蛇小游戏(附完整代码) 用HTML5 Canvas从零实现贪吃蛇小游戏附完整代码Canvas技术作为HTML5的核心组件之一为开发者提供了强大的图形绘制能力。本文将带您从零开始通过实现经典的贪吃蛇游戏全面掌握Canvas API的实际应用。不同于简单的代码复制我们会深入解析每个技术细节帮助前端初学者真正理解Canvas的工作原理。1. 项目准备与环境搭建在开始编码之前我们需要先了解Canvas的基本概念。Canvas是一个可以通过JavaScript绘制图形的HTML元素它提供了丰富的API用于绘制路径、矩形、圆形、字符以及添加图像。首先创建一个基础的HTML文件结构!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title贪吃蛇小游戏/title style body { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #f0f0f0; } #gameCanvas { border: 1px solid #333; background-color: #fff; } .controls { margin-top: 20px; } /style /head body canvas idgameCanvas width400 height400/canvas div classcontrols button idstartBtn开始游戏/button button idresetBtn disabled重新开始/button /div script srcgame.js/script /body /html提示将CSS样式与JavaScript代码分离是良好的开发习惯有助于代码维护和扩展。2. Canvas基础与游戏初始化Canvas的绘图主要通过获取其上下文(Context)对象来实现。对于2D游戏我们使用getContext(2d)方法获取2D渲染上下文。在game.js中我们首先初始化游戏所需的基本变量// 获取Canvas元素和上下文 const canvas document.getElementById(gameCanvas); const ctx canvas.getContext(2d); // 游戏配置 const GRID_SIZE 20; // 每个格子的大小 const GRID_WIDTH canvas.width / GRID_SIZE; const GRID_HEIGHT canvas.height / GRID_SIZE; // 游戏状态 let snake []; // 蛇的身体 let food {}; // 食物位置 let direction RIGHT; // 当前移动方向 let nextDirection RIGHT; // 下一帧的移动方向 let gameSpeed 150; // 游戏速度(毫秒) let gameLoop; // 游戏循环引用 let score 0; // 游戏得分接下来我们创建游戏的初始化函数function initGame() { // 初始化蛇的位置 snake [ {x: 5, y: 10}, {x: 4, y: 10}, {x: 3, y: 10} ]; // 随机生成食物位置 generateFood(); // 重置游戏状态 direction RIGHT; nextDirection RIGHT; score 0; // 绘制初始游戏画面 drawGame(); }3. 游戏核心逻辑实现贪吃蛇游戏的核心逻辑包括蛇的移动、碰撞检测和食物生成。让我们分别实现这些功能。3.1 蛇的移动控制蛇的移动通过改变其身体各节的位置来实现。我们需要处理方向控制和防止180度急转弯function updateSnake() { // 获取蛇头 const head {x: snake[0].x, y: snake[0].y}; // 根据方向更新蛇头位置 switch(direction) { case UP: head.y - 1; break; case DOWN: head.y 1; break; case LEFT: head.x - 1; break; case RIGHT: head.x 1; break; } // 将新头部添加到蛇身 snake.unshift(head); // 检查是否吃到食物 if(head.x food.x head.y food.y) { // 吃到食物不删除尾部蛇变长 score 10; generateFood(); } else { // 没吃到食物删除尾部 snake.pop(); } // 更新方向为缓冲方向(防止连续按键导致的180度转弯) direction nextDirection; }3.2 碰撞检测游戏需要检测蛇是否撞到墙壁或自身function checkCollision() { const head snake[0]; // 检查撞墙 if(head.x 0 || head.x GRID_WIDTH || head.y 0 || head.y GRID_HEIGHT) { return true; } // 检查撞到自己(从第1节开始检查跳过头部) for(let i 1; i snake.length; i) { if(head.x snake[i].x head.y snake[i].y) { return true; } } return false; }3.3 食物生成食物需要在游戏区域内随机生成但不能出现在蛇身上function generateFood() { let positionValid false; let newFood {}; while(!positionValid) { newFood { x: Math.floor(Math.random() * GRID_WIDTH), y: Math.floor(Math.random() * GRID_HEIGHT) }; // 检查食物是否出现在蛇身上 positionValid true; for(let segment of snake) { if(segment.x newFood.x segment.y newFood.y) { positionValid false; break; } } } food newFood; }4. 游戏渲染与用户交互良好的视觉效果和流畅的交互是游戏体验的关键。我们将实现游戏的绘制和用户输入处理。4.1 游戏画面绘制使用Canvas API绘制游戏元素function drawGame() { // 清空画布 ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制蛇 snake.forEach((segment, index) { ctx.fillStyle index 0 ? #4CAF50 : #8BC34A; // 头部和身体不同颜色 ctx.fillRect( segment.x * GRID_SIZE, segment.y * GRID_SIZE, GRID_SIZE - 1, GRID_SIZE - 1 ); }); // 绘制食物 ctx.fillStyle #FF5722; ctx.beginPath(); ctx.arc( food.x * GRID_SIZE GRID_SIZE / 2, food.y * GRID_SIZE GRID_SIZE / 2, GRID_SIZE / 2 - 1, 0, Math.PI * 2 ); ctx.fill(); // 绘制分数 ctx.fillStyle #333; ctx.font 20px Arial; ctx.fillText(分数: ${score}, 10, 30); }4.2 用户输入处理为游戏添加键盘和按钮控制// 键盘控制 document.addEventListener(keydown, (e) { switch(e.key) { case ArrowUp: if(direction ! DOWN) nextDirection UP; break; case ArrowDown: if(direction ! UP) nextDirection DOWN; break; case ArrowLeft: if(direction ! RIGHT) nextDirection LEFT; break; case ArrowRight: if(direction ! LEFT) nextDirection RIGHT; break; } }); // 游戏控制按钮 document.getElementById(startBtn).addEventListener(click, () { initGame(); document.getElementById(startBtn).disabled true; document.getElementById(resetBtn).disabled false; gameLoop setInterval(gameStep, gameSpeed); }); document.getElementById(resetBtn).addEventListener(click, () { clearInterval(gameLoop); initGame(); gameLoop setInterval(gameStep, gameSpeed); }); // 游戏主循环 function gameStep() { updateSnake(); if(checkCollision()) { clearInterval(gameLoop); alert(游戏结束! 你的得分: ${score}); document.getElementById(startBtn).disabled false; document.getElementById(resetBtn).disabled true; return; } drawGame(); }5. 游戏优化与扩展基础功能完成后我们可以考虑一些优化和扩展功能提升游戏体验。5.1 游戏难度调整随着分数增加提高游戏速度function updateGameSpeed() { // 每得100分速度提高10% const speedIncrease Math.floor(score / 100); const newSpeed Math.max(50, gameSpeed - speedIncrease * 15); if(newSpeed ! gameSpeed) { gameSpeed newSpeed; clearInterval(gameLoop); gameLoop setInterval(gameStep, gameSpeed); } }5.2 添加特殊食物引入不同类型的食物增加游戏趣味性let foodType normal; // normal, bonus, speed function generateFood() { // ...之前的生成逻辑... // 随机决定食物类型 const rand Math.random(); if(rand 0.1) { foodType bonus; // 10%几率生成奖励食物 } else if(rand 0.2) { foodType speed; // 10%几率生成速度食物 } else { foodType normal; } } // 在drawGame中根据食物类型绘制不同样式 function drawGame() { // ...其他绘制代码... switch(foodType) { case bonus: ctx.fillStyle #FFC107; break; case speed: ctx.fillStyle #2196F3; break; default: ctx.fillStyle #FF5722; } // ...绘制食物... }5.3 添加游戏音效使用Web Audio API为游戏添加音效// 音效初始化 const audioContext new (window.AudioContext || window.webkitAudioContext)(); let eatSound; function initAudio() { // 创建简单的音效 const oscillator audioContext.createOscillator(); const gainNode audioContext.createGain(); oscillator.type triangle; oscillator.frequency.value 880; gainNode.gain.value 0.5; oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.start(); gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime 0.3); oscillator.stop(audioContext.currentTime 0.3); } // 在吃到食物时播放音效 function updateSnake() { // ...之前的代码... if(head.x food.x head.y food.y) { initAudio(); // 播放吃食物音效 // ...其他逻辑... } }6. 完整代码与项目结构将所有代码整合后我们的项目结构如下/snake-game │ index.html │ game.js │ style.css (可选)完整JavaScript代码(game.js)// 游戏配置和状态 const canvas document.getElementById(gameCanvas); const ctx canvas.getContext(2d); const GRID_SIZE 20; const GRID_WIDTH canvas.width / GRID_SIZE; const GRID_HEIGHT canvas.height / GRID_SIZE; let snake []; let food {}; let direction RIGHT; let nextDirection RIGHT; let gameSpeed 150; let gameLoop; let score 0; let foodType normal; // 初始化游戏 function initGame() { snake [ {x: 5, y: 10}, {x: 4, y: 10}, {x: 3, y: 10} ]; generateFood(); direction RIGHT; nextDirection RIGHT; score 0; gameSpeed 150; drawGame(); } // 生成食物 function generateFood() { let positionValid false; let newFood {}; while(!positionValid) { newFood { x: Math.floor(Math.random() * GRID_WIDTH), y: Math.floor(Math.random() * GRID_HEIGHT) }; positionValid true; for(let segment of snake) { if(segment.x newFood.x segment.y newFood.y) { positionValid false; break; } } } food newFood; foodType Math.random() 0.1 ? bonus : (Math.random() 0.2 ? speed : normal); } // 更新蛇位置 function updateSnake() { const head {x: snake[0].x, y: snake[0].y}; switch(direction) { case UP: head.y - 1; break; case DOWN: head.y 1; break; case LEFT: head.x - 1; break; case RIGHT: head.x 1; break; } snake.unshift(head); if(head.x food.x head.y food.y) { initAudio(); score foodType bonus ? 30 : (foodType speed ? 15 : 10); generateFood(); updateGameSpeed(); } else { snake.pop(); } direction nextDirection; } // 检查碰撞 function checkCollision() { const head snake[0]; if(head.x 0 || head.x GRID_WIDTH || head.y 0 || head.y GRID_HEIGHT) { return true; } for(let i 1; i snake.length; i) { if(head.x snake[i].x head.y snake[i].y) { return true; } } return false; } // 更新游戏速度 function updateGameSpeed() { const speedIncrease Math.floor(score / 100); const newSpeed Math.max(50, gameSpeed - speedIncrease * 15); if(newSpeed ! gameSpeed) { gameSpeed newSpeed; clearInterval(gameLoop); gameLoop setInterval(gameStep, gameSpeed); } } // 绘制游戏 function drawGame() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制蛇 snake.forEach((segment, index) { ctx.fillStyle index 0 ? #4CAF50 : #8BC34A; ctx.fillRect( segment.x * GRID_SIZE, segment.y * GRID_SIZE, GRID_SIZE - 1, GRID_SIZE - 1 ); }); // 绘制食物 switch(foodType) { case bonus: ctx.fillStyle #FFC107; break; case speed: ctx.fillStyle #2196F3; break; default: ctx.fillStyle #FF5722; } ctx.beginPath(); ctx.arc( food.x * GRID_SIZE GRID_SIZE / 2, food.y * GRID_SIZE GRID_SIZE / 2, GRID_SIZE / 2 - 1, 0, Math.PI * 2 ); ctx.fill(); // 绘制分数 ctx.fillStyle #333; ctx.font 20px Arial; ctx.fillText(分数: ${score}, 10, 30); } // 游戏主循环 function gameStep() { updateSnake(); if(checkCollision()) { clearInterval(gameLoop); alert(游戏结束! 你的得分: ${score}); document.getElementById(startBtn).disabled false; document.getElementById(resetBtn).disabled true; return; } drawGame(); } // 音效 function initAudio() { const audioContext new (window.AudioContext || window.webkitAudioContext)(); const oscillator audioContext.createOscillator(); const gainNode audioContext.createGain(); oscillator.type triangle; oscillator.frequency.value 880; gainNode.gain.value 0.5; oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.start(); gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime 0.3); oscillator.stop(audioContext.currentTime 0.3); } // 事件监听 document.addEventListener(keydown, (e) { switch(e.key) { case ArrowUp: if(direction ! DOWN) nextDirection UP; break; case ArrowDown: if(direction ! UP) nextDirection DOWN; break; case ArrowLeft: if(direction ! RIGHT) nextDirection LEFT; break; case ArrowRight: if(direction ! LEFT) nextDirection RIGHT; break; } }); document.getElementById(startBtn).addEventListener(click, () { initGame(); document.getElementById(startBtn).disabled true; document.getElementById(resetBtn).disabled false; gameLoop setInterval(gameStep, gameSpeed); }); document.getElementById(resetBtn).addEventListener(click, () { clearInterval(gameLoop); initGame(); gameLoop setInterval(gameStep, gameSpeed); }); // 初始化游戏 initGame();