引言


贪吃蛇游戏是一款经典的小游戏,简单易上手却又充满挑战。本文将详细介绍一个基于 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 面向对象编程的一个很好的示例。你可以根据自己的需求对代码进行扩展和优化,例如添加更多的游戏模式、优化界面样式等。

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐