贪吃蛇小游戏开发(html+js+css)
snake.html :作为游戏的入口文件,负责搭建 HTML 结构,引入样式和脚本文件。style.css :用于定义游戏的样式,包括布局、颜色、动画效果等。game.js :实现游戏的核心逻辑,如蛇的移动、食物的生成、碰撞检测等。通过 snake.html 、 style.css 和 game.js 三个文件的协同工作,我们实现了一个简单而有趣的贪吃蛇游戏。html 负责搭建页面结构, css
·
引言
贪吃蛇游戏是一款经典的小游戏,简单易上手却又充满挑战。本文将详细介绍一个基于 HTML、CSS 和 JavaScript 实现的贪吃蛇游戏,包括 game.js 、 snake.html 和 style.css 三个核心文件的代码解析和功能实现。
项目结构概述
本项目组成:
- snake.html :作为游戏的入口文件,负责搭建 HTML 结构,引入样式和脚本文件。
- style.css :用于定义游戏的样式,包括布局、颜色、动画效果等。
- game.js :实现游戏的核心逻辑,如蛇的移动、食物的生成、碰撞检测等。
1. snake.html 文件
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<!-- 设置字符编码 -->
<meta charset="UTF-8">
<!-- 适配移动端 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 设置页面标题 -->
<title>贪吃蛇游戏</title>
<!-- 引入样式文件 -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- 游戏主容器 -->
<div class="game-container">
<header>
<!-- 游戏标题 -->
<h1>🐍 贪吃蛇游戏 🐍</h1>
<!-- 游戏操作说明 -->
<p class="instructions">使用方向键 ↑ ↓ ← → 控制蛇的移动</p>
</header>
<main>
<!-- 游戏画布 -->
<canvas id="gameCanvas" width="400" height="400"></canvas>
<!-- 控制面板 -->
<div class="controls">
<!-- 减速按钮 -->
<button class="btn" id="slowBtn">⏪ 减速</button>
<!-- 速度显示区域 -->
<span id="speedDisplay" class="speed-display">速度: 中</span>
<!-- 加速按钮 -->
<button class="btn" id="fastBtn">⏩ 加速</button>
<!-- 暂停/继续按钮 -->
<button class="btn" id="pauseBtn">⏸️ 暂停</button>
</div>
</main>
</div>
<!-- 引入游戏脚本 -->
<script src="game.js"></script>
<script>
// 初始化游戏实例
const game = new Game(document.getElementById('gameCanvas'));
</script>
</body>
</html>
2. game.js 文件
// 蛇类定义
class Snake {
constructor() {
// 每个格子的大小
this.box = 20;
// 蛇身初始位置
this.body = [{x: 9 * this.box, y: 10 * this.box}];
// 当前移动方向
this.direction = null;
// 得分
this.score = 0;
// 速度等级数组
this.speedLevels = [50, 80, 120, 160, 200];
// 默认速度等级(中速)
this.currentSpeed = 2;
}
/**
* 获取当前游戏速度
*/
get gameSpeed() {
return this.speedLevels[this.currentSpeed];
}
/**
* 调整速度等级
* @param {number} amount 速度变化量
*/
adjustSpeed(amount) {
this.currentSpeed = Math.max(0, Math.min(this.speedLevels.length - 1, this.currentSpeed + amount));
}
/**
* 移动蛇头
* @returns {Object} 新的蛇头位置
*/
move() {
// 复制当前蛇头位置
let head = {...this.body[0]};
// 根据方向更新蛇头位置
if (this.direction === 'LEFT') head.x -= this.box;
if (this.direction === 'UP') head.y -= this.box;
if (this.direction === 'RIGHT') head.x += this.box;
if (this.direction === 'DOWN') head.y += this.box;
return head;
}
/**
* 增加蛇长和分数
*/
grow() {
this.score++;
}
/**
* 检测碰撞
* @param {Object} head 蛇头位置
* @param {number} canvasWidth 画布宽度
* @param {number} canvasHeight 画布高度
* @returns {boolean} 是否发生碰撞
*/
checkCollision(head, canvasWidth, canvasHeight) {
return (
head.x < 0 || head.x >= canvasWidth ||
head.y < 0 || head.y >= canvasHeight ||
this.body.some(segment => head.x === segment.x && head.y === segment.y)
);
}
}
// 食物类定义
class Food {
constructor(boxSize) {
// 格子大小
this.box = boxSize;
// 生成随机位置
this.position = this.generatePosition();
}
/**
* 生成随机位置
* @returns {Object} 食物位置
*/
generatePosition() {
return {
x: Math.floor(Math.random() * 20) * this.box,
y: Math.floor(Math.random() * 20) * this.box
};
}
/**
* 检测是否被吃掉
* @param {Object} snakeHead 蛇头位置
* @returns {boolean} 是否被吃掉
*/
isEaten(snakeHead) {
return snakeHead.x === this.position.x && snakeHead.y === this.position.y;
}
}
// 粒子类定义
class Particle {
constructor(x, y) {
// 粒子位置
this.x = x;
this.y = y;
// 粒子大小
this.size = Math.random() * 3 + 2;
// 粒子水平速度
this.vx = Math.random() * 6 - 3;
// 粒子垂直速度
this.vy = Math.random() * 6 - 3;
// 粒子颜色
this.color = `hsl(${Math.random() * 60 + 10}, 100%, 50%)`;
// 粒子透明度
this.alpha = 0.8;
}
/**
* 更新粒子状态
*/
update() {
this.x += this.vx;
this.y += this.vy;
this.alpha -= 0.01;
this.size *= 0.98;
}
/**
* 绘制粒子
* @param {CanvasRenderingContext2D} ctx 画布上下文
*/
draw(ctx) {
ctx.fillStyle = this.color;
ctx.globalAlpha = this.alpha;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
}
// 游戏类定义
class Game {
constructor(canvas) {
// 绑定触摸事件处理函数
this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleTouchEnd = this.handleTouchEnd.bind(this);
// 游戏画布
this.canvas = canvas;
// 画布上下文
this.ctx = canvas.getContext('2d');
// 蛇实例
this.snake = new Snake();
// 食物实例
this.food = new Food(this.snake.box);
// 游戏速度
this.gameSpeed = 100;
// 粒子数组
this.particles = [];
// 上次渲染时间
this.lastRenderTime = 0;
// 游戏暂停状态
this.isPaused = false;
// 音效
this.eatSound = new Audio('https://assets.mixkit.co/sfx/preview/mixkit-arcade-game-jump-coin-216.mp3');
this.gameOverSound = new Audio('https://assets.mixkit.co/sfx/preview/mixkit-retro-arcade-lose-2027.mp3');
// 初始化操作
this.init();
// 设置事件监听器
this.setupEventListeners();
}
/**
* 设置事件监听器
*/
setupEventListeners() {
// 为触控按钮添加触摸事件监听器
document.querySelectorAll('.touch-btn').forEach(btn => {
btn.addEventListener('touchstart', this.handleTouchStart);
btn.addEventListener('touchend', this.handleTouchEnd);
});
// 为减速按钮添加点击事件监听器
document.getElementById('slowBtn').addEventListener('click', () => this.snake.adjustSpeed(-1));
// 为加速按钮添加点击事件监听器
document.getElementById('fastBtn').addEventListener('click', () => this.snake.adjustSpeed(1));
// 为暂停按钮添加点击事件监听器
document.getElementById('pauseBtn').addEventListener('click', () => this.togglePause());
}
/**
* 处理触摸开始事件
* @param {TouchEvent} e 触摸事件对象
*/
handleTouchStart(e) {
const btn = e.target.closest('.touch-btn');
if (!btn) return;
// 改变按钮样式
btn.style.transform = 'scale(0.85)';
btn.style.backgroundColor = 'rgba(255, 255, 255, 0.4)';
// 改变蛇的移动方向
this.changeDirection(btn.classList.contains('up') ? 'UP' :
btn.classList.contains('down') ? 'DOWN' :
btn.classList.contains('left') ? 'LEFT' : 'RIGHT');
}
/**
* 处理触摸结束事件
* @param {TouchEvent} e 触摸事件对象
*/
handleTouchEnd(e) {
const btn = e.target.closest('.touch-btn');
if (!btn) return;
// 恢复按钮样式
btn.style.transform = 'scale(1)';
btn.style.backgroundColor = 'rgba(255, 255, 255, 0.2)';
}
/**
* 初始化操作,添加 roundRect 兼容方法
*/
init() {
if (!CanvasRenderingContext2D.prototype.roundRect) {
CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
this.beginPath();
this.moveTo(x + r, y);
this.arcTo(x + w, y, x + w, y + h, r);
this.arcTo(x + w, y + h, x, y + h, r);
this.arcTo(x, y + h, x, y, r);
this.arcTo(x, y, x + w, y, r);
this.closePath();
return this;
}
}
}
/**
* 开始游戏
*/
start() {
this.gameLoop();
}
/**
* 游戏主循环
* @param {number} currentTime 当前时间
*/
gameLoop(currentTime = 0) {
if (!this.snake.direction || this.isPaused) {
this.draw();
requestAnimationFrame((time) => this.gameLoop(time));
return;
}
const secondsSinceLastRender = (currentTime - this.lastRenderTime) / 1000;
if (secondsSinceLastRender < 1 / (this.gameSpeed / 10)) {
requestAnimationFrame((time) => this.gameLoop(time));
return;
}
this.lastRenderTime = currentTime;
const newHead = this.snake.move();
if (this.food.isEaten(newHead)) {
this.eatSound.play();
this.snake.grow();
this.createParticles();
this.food = new Food(this.snake.box);
} else {
this.snake.body.pop();
}
if (this.snake.checkCollision(newHead, this.canvas.width, this.canvas.height)) {
this.gameOver();
return;
}
this.snake.body.unshift(newHead);
this.draw();
requestAnimationFrame((time) => this.gameLoop(time));
}
/**
* 绘制游戏元素
*/
draw() {
this.drawParticles();
this.drawBackground();
this.drawSnake();
this.drawFood();
this.drawScore();
}
/**
* 绘制粒子效果
*/
drawParticles() {
this.particles.forEach(particle => {
particle.draw(this.ctx);
particle.update();
});
this.particles = this.particles.filter(p => p.alpha > 0 && p.size > 0.1);
this.ctx.globalAlpha = 1;
}
/**
* 绘制游戏背景
*/
drawBackground() {
this.ctx.fillStyle = '#2c3e50';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
this.ctx.lineWidth = 0.5;
for(let i = 0; i < this.canvas.width; i += this.snake.box) {
this.ctx.beginPath();
this.ctx.moveTo(i, 0);
this.ctx.lineTo(i, this.canvas.height);
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.moveTo(0, i);
this.ctx.lineTo(this.canvas.width, i);
this.ctx.stroke();
}
}
/**
* 绘制蛇
*/
drawSnake() {
for (let i = 0; i < this.snake.body.length; i++) {
this.ctx.shadowColor = 'rgba(46, 204, 113, 0.5)';
this.ctx.shadowBlur = 15 - i*2;
this.ctx.shadowOffsetX = 0;
this.ctx.shadowOffsetY = 0;
this.ctx.beginPath();
if (i === 0) {
this.ctx.fillStyle = '#2ecc71';
this.ctx.roundRect(this.snake.body[i].x, this.snake.body[i].y, this.snake.box, this.snake.box, [this.snake.box/2, this.snake.box/2, this.snake.box/5, this.snake.box/5]);
} else {
const gradient = this.ctx.createLinearGradient(
this.snake.body[i].x, this.snake.body[i].y,
this.snake.body[i].x + this.snake.box, this.snake.body[i].y + this.snake.box
);
gradient.addColorStop(0, `hsl(145, 60%, ${45 - i*2}%)`);
gradient.addColorStop(1, `hsl(145, 70%, ${35 - i*2}%)`);
this.ctx.fillStyle = gradient;
this.ctx.roundRect(this.snake.body[i].x, this.snake.body[i].y, this.snake.box, this.snake.box, this.snake.box/3);
}
this.ctx.fill();
this.ctx.shadowColor = this.ctx.fillStyle;
this.ctx.shadowBlur = 5;
}
}
/**
* 绘制食物
*/
drawFood() {
this.ctx.beginPath();
this.ctx.fillStyle = '#e74c3c';
const scale = 0.8 + Math.sin(Date.now()/150) * 0.2;
this.ctx.save();
this.ctx.translate(this.food.position.x + this.snake.box/2, this.food.position.y + this.snake.box/2);
this.ctx.scale(scale, scale);
this.ctx.arc(0, 0, this.snake.box/2, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.restore();
this.ctx.shadowColor = '#e74c3c';
this.ctx.shadowBlur = 10;
}
/**
* 绘制分数
*/
drawScore() {
this.ctx.fillStyle = '#ecf0f1';
this.ctx.font = 'bold 20px Arial';
this.ctx.fillText('分数: ' + this.snake.score, 10, 30);
}
/**
* 创建粒子效果
*/
createParticles() {
for (let i = 0; i < 15; i++) {
this.particles.push(new Particle(
this.food.position.x + this.snake.box/2,
this.food.position.y + this.snake.box/2
));
}
}
/**
* 游戏结束处理
*/
gameOver() {
let highScore = localStorage.getItem('snakeHighScore') || 0;
if(this.snake.score > highScore) {
localStorage.setItem('snakeHighScore', this.snake.score);
highScore = this.snake.score;
}
this.gameOverSound.play();
this.showGameOverModal(this.snake.score, highScore);
}
/**
* 显示游戏结束模态框
* @param {number} score 当前得分
* @param {number} highScore 历史最高分
*/
showGameOverModal(score, highScore) {
const modal = document.createElement('div');
modal.className = 'game-over-modal';
modal.innerHTML = `
<div class="modal-content">
<h2>游戏结束!</h2>
<p>本次得分: ${score}</p>
<p>历史最高分: ${highScore}</p>
<button class="btn" onclick="document.location.reload()">再玩一次</button>
</div>
`;
document.body.appendChild(modal);
}
/**
* 改变游戏速度
* @param {number} amount 速度变化量
*/
changeSpeed(amount) {
this.gameSpeed = Math.max(30, Math.min(200, this.gameSpeed + amount));
this.updateSpeedDisplay();
}
/**
* 更新速度显示
*/
updateSpeedDisplay() {
let speedText = '速度: ';
if (this.gameSpeed <= 50) speedText += '极慢';
else if (this.gameSpeed <= 80) speedText += '慢';
else if (this.gameSpeed <= 120) speedText += '中';
else if (this.gameSpeed <= 160) speedText += '快';
else speedText += '极快';
document.getElementById('speedDisplay').textContent = speedText;
}
/**
* 改变蛇的移动方向
* @param {string} newDir 新的移动方向
*/
changeDirection(newDir) {
if ((newDir === 'LEFT' && this.snake.direction !== 'RIGHT') ||
(newDir === 'UP' && this.snake.direction !== 'DOWN') ||
(newDir === 'RIGHT' && this.snake.direction !== 'LEFT') ||
(newDir === 'DOWN' && this.snake.direction !== 'UP')) {
this.snake.direction = newDir;
}
}
/**
* 切换游戏暂停状态
*/
togglePause() {
this.isPaused = !this.isPaused;
document.getElementById('pauseBtn').textContent = this.isPaused ? '▶️ 继续' : '⏸️ 暂停';
}
}
// 初始化游戏
const game = new Game(document.getElementById('gameCanvas'));
document.addEventListener('keydown', (event) => {
if(event.repeat) return;
if (['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown'].includes(event.key)) {
event.preventDefault();
}
if (event.key === 'ArrowLeft' && game.snake.direction !== 'RIGHT') {
game.snake.direction = 'LEFT';
} else if (event.key === 'ArrowUp' && game.snake.direction !== 'DOWN') {
game.snake.direction = 'UP';
} else if (event.key === 'ArrowRight' && game.snake.direction !== 'LEFT') {
game.snake.direction = 'RIGHT';
} else if (event.key === 'ArrowDown' && game.snake.direction !== 'UP') {
game.snake.direction = 'DOWN';
}
});
// 开始游戏
game.start();
3. style.css 文件
/* 定义全局变量,方便统一管理颜色和样式参数 */
:root {
--primary-color: #3498db;
--primary-hover: #2980b9;
--snake-head: #2ecc71;
--snake-body: #27ae60;
--food-color: #e74c3c;
--bg-dark: #2c3e50;
--text-light: #ecf0f1;
--text-dark: #2c3e50;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
/* 粒子效果参数 */
--particle-size: 2;
--particle-speed: 3;
--particle-alpha: 0.8;
--particle-fade: 0.01;
}
/* 暗黑主题样式 */
[data-theme="dark"] {
--primary-color: #9b59b6;
--primary-hover: #8e44ad;
--snake-head: #3498db;
--snake-body: #2980b9;
--food-color: #e74c3c;
--bg-dark: #1a1a1a;
--text-light: #ecf0f1;
--text-dark: #ecf0f1;
}
/* 复古主题样式 */
[data-theme="retro"] {
--primary-color: #f39c12;
--primary-hover: #e67e22;
--snake-head: #16a085;
--snake-body: #1abc9c;
--food-color: #c0392b;
--bg-dark: #34495e;
--text-light: #ecf0f1;
--text-dark: #2c3e50;
}
/* 全局 body 样式 */
body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
margin: 0;
padding: 2rem;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
color: var(--text-dark);
}
/* 游戏容器样式 */
.game-container {
max-width: 500px;
width: 100%;
}
/* 头部样式 */
header {
text-align: center;
margin-bottom: 1.5rem;
}
/* 标题样式 */
h1 {
color: var(--text-dark);
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
margin: 0 0 0.5rem;
font-size: 2rem;
}
/* 游戏说明样式 */
.instructions {
color: #7f8c8d;
margin: 0;
font-size: 0.9rem;
}
/* 画布样式 */
canvas {
border: 3px solid var(--bg-dark);
border-radius: 10px;
box-shadow: var(--shadow);
background-color: var(--bg-dark);
width: 100%;
aspect-ratio: 1/1;
display: block;
margin: 0 auto;
}
/* 控制面板样式 */
.controls {
margin: 1.5rem 0;
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
/* 游戏结束模态框样式 */
.game-over-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
/* 模态框内容样式 */
.modal-content {
background: var(--bg-dark);
padding: 2rem;
border-radius: 10px;
text-align: center;
color: var(--text-light);
box-shadow: var(--shadow);
}
/* 模态框标题样式 */
.modal-content h2 {
margin-top: 0;
color: var(--text-light);
}
/* 模态框段落样式 */
.modal-content p {
margin: 1rem 0;
font-size: 1.2rem;
}
/* 按钮通用样式 */
.btn {
padding: 0.5rem 1rem;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s ease;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
/* 按钮悬停样式 */
.btn:hover {
background-color: var(--primary-hover);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
/* 按钮激活样式 */
.btn:active {
transform: scale(0.95) translateY(-1px);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
/* 触控按钮激活样式 */
.touch-btn:active {
background: rgba(255, 255, 255, 0.3);
transform: scale(0.9);
}
/* 移动端触控按钮容器样式 */
.touch-controls {
display: none;
gap: 10px;
justify-content: center;
margin: 1rem 0;
}
/* 触控按钮样式 */
.touch-btn {
width: 50px;
height: 50px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
border: 2px solid var(--primary-color);
font-size: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
touch-action: manipulation;
}
/* 移动端适配样式 */
@media (max-width: 480px) {
.touch-controls {
display: flex;
flex-direction: column;
}
.controls > .btn:not(.touch-btn) {
display: none;
}
body {
padding: 1rem;
}
h1 {
font-size: 1.5rem;
}
.controls {
flex-direction: column;
gap: 0.75rem;
}
}
/* 速度显示样式 */
.speed-display {
font-weight: bold;
color: var(--text-dark);
background-color: var(--text-light);
padding: 0.5rem 1rem;
border-radius: 5px;
min-width: 80px;
display: inline-block;
}
总结
通过 snake.html 、 style.css 和 game.js 三个文件的协同工作,我们实现了一个简单而有趣的贪吃蛇游戏。 html 负责搭建页面结构, css 负责样式设计, js 负责实现游戏的核心逻辑。这个项目不仅展示了前端开发的基本技能,还可以作为学习 JavaScript 面向对象编程的一个很好的示例。你可以根据自己的需求对代码进行扩展和优化,例如添加更多的游戏模式、优化界面样式等。
更多推荐
所有评论(0)