前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎 点赞 + 收藏 + 关注 哦 💕

(文后附完整代码)html+css+javascript 切水果游戏项目分析

📚 效果展示

(文后附完整代码)html+css+javascript 切水果游戏项目分析

📚 本文简介

本文介绍了一个基于HTML5 Canvas的切水果游戏开发项目,详细解析了其技术架构和实现方法。游戏采用三层结构设计,结合HTML5 Canvas渲染、CSS3动画效果和JavaScript游戏逻辑,实现了水果切割、得分系统、连击特效等核心玩法。文章从项目定位、技术栈、HTML结构、CSS样式到JavaScript核心逻辑进行了系统分析,并提供了完整代码示例,帮助开发者深入理解休闲游戏开发的全流程实现。

目录

 

———— ⬇️·`正文开始`·⬇️————

 

📚 一、项目架构概述

📘 1.1 项目定位与核心玩法

切水果游戏是一款经典的休闲小游戏,玩家通过鼠标或手指在屏幕上滑动来切割从底部弹出的水果,获得分数。游戏融合了物理引擎、碰撞检测、视觉特效和音效系统,提供流畅的游戏体验。

核心玩法特点:

  • 水果从底部抛物线弹出,玩家需在水果下落前切割
  • 支持连击系统,连续切割可获得额外分数加成
  • 包含炸弹道具,误切会扣分
  • 60秒限时挑战,追求最高分

📘 1.2 技术栈与架构模式

前端技术栈:

  • HTML5 Canvas API:核心渲染引擎
  • Web Audio API:音效生成与处理
  • JavaScript ES6+:游戏逻辑实现
  • CSS3:UI样式与动画效果
  • LocalStorage:本地数据持久化

架构模式:

  • 单文件架构:所有代码集成在一个HTML文件中
  • 状态机模式:游戏状态管理(运行/暂停/结束)
  • 面向对象思想:水果对象、游戏状态对象
  • 事件驱动:鼠标/触摸事件交互

📚 二、HTML结构分析:语义化与模块化的界面搭建

📘 2.1 整体布局设计

游戏采用三层结构设计:

┌─────────────────────────────────┐
│         游戏容器 (gameContainer) │
│  ┌─────────────────────────────┐ │
│  │   Canvas画布 (gameCanvas)   │ │ ← 底层渲染
│  └─────────────────────────────┘ │
│  ┌─────────────────────────────┐ │
│  │   UI层 (得分/时间/最高分)   │ │ ← 中层信息
│  └─────────────────────────────┘ │
│  ┌─────────────────────────────┐ │
│  │   剑光标 (sword)           │ │ ← 顶层交互
│  └─────────────────────────────┘ │
│  ┌─────────────────────────────┐ │
│  │   开始/结束界面             │ │ ← 遮罩层
│  └─────────────────────────────┘ │
└─────────────────────────────────┘

📘 2.2 关键元素解析

📖 2.2.1 Canvas元素的渲染设计
<canvas id="gameCanvas"></canvas>

作用:

  • 900x700像素的游戏画布
  • 负责渲染水果、切割轨迹、背景等所有动态元素
  • 通过2D上下文(ctx)进行绘制操作

初始化代码:

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = gameContainer.offsetWidth;
canvas.height = gameContainer.offsetHeight;
📖 2.2.2 UI信息层的设计
<div id="ui">
    <div>得分: <span id="score">0</span></div>
    <div>时间: <span id="time">60</span>秒</div>
    <div>最高分: <span id="highScore">0</span></div>
</div>

设计特点:

  • 绝对定位在游戏容器顶部
  • 左右分布三个信息面板
  • pointer-events: none确保不干扰游戏交互
  • 白色文字配黑色阴影,增强可读性
📖 2.2.3 状态遮罩层的设计

开始界面:

<div id="startScreen">
    <h1>🍎 切水果游戏 🍎</h1>
    <p>用鼠标或手指滑动来切水果!</p>
    <button id="startButton">开始游戏</button>
</div>

结束界面:

<div id="gameOverScreen">
    <h2>游戏结束</h2>
    <div id="finalScore">得分: 0</div>
    <button id="restartButton">重新开始</button>
</div>

设计特点:

  • 全屏黑色半透明遮罩 (rgba(0,0,0,0.8))
  • 垂直居中布局
  • 高z-index确保覆盖所有内容
  • 开始界面默认显示,结束界面默认隐藏

📚 三、CSS样式分析:视觉效果与交互体验

📘 3.1 响应式布局与容器设计

#gameContainer {
    position: relative;
    width: 900px;
    height: 700px;
    background: linear-gradient(to bottom, #87CEEB 0%, #98D8C8 50%, #F7DC6F 100%);
    border: 5px solid #fff;
    border-radius: 20px;
    box-shadow: 0 15px 40px rgba(0, 0, 0, 0.5);
    overflow: hidden;
    cursor: none;
}

关键属性解析:

  • position: relative:为内部绝对定位元素提供参考
  • background: linear-gradient:天空到草地的渐变背景
  • overflow: hidden:防止水果溢出显示
  • cursor: none:隐藏系统光标,使用自定义剑光标

📘 3.2 动画效果系统

📖 3.2.1 特效动画(Effect)
.effect {
    position: absolute;
    font-size: 60px;
    animation: floatUp 1.2s ease-out forwards;
}

@keyframes floatUp {
    0% { opacity: 1; transform: translateY(0) scale(1) rotate(0deg); }
    100% { opacity: 0; transform: translateY(-200px) scale(1.8) rotate(360deg); }
}

应用场景: 切割水果时的星星特效(⭐✨💫等)

📖 3.2.2 得分弹出动画(Score Popup)
.score-popup {
    font-size: 36px;
    font-weight: bold;
    color: #FFD700;
    animation: scoreFloat 1s ease-out forwards;
}

@keyframes scoreFloat {
    0% { opacity: 1; transform: translateY(0) scale(1); }
    100% { opacity: 0; transform: translateY(-120px) scale(1.5); }
}

设计特点:

  • 金色文字配黑色阴影
  • 向上漂浮并放大
  • 1秒后自动消失
📖 3.2.3 连击提示动画(Combo Popup)
.combo-popup {
    font-size: 48px;
    color: #FF6B6B;
    animation: comboFloat 1.5s ease-out forwards;
}

@keyframes comboFloat {
    0% { opacity: 1; transform: translateY(0) scale(0.5); }
    50% { opacity: 1; transform: translateY(-50px) scale(1.2); }
    100% { opacity: 0; transform: translateY(-150px) scale(1); }
}

设计特点:

  • 红色醒目文字
  • 先放大后缩小的弹性效果
  • 1.5秒持续时间,强调连击成就感

📘 3.3 剑光标交互设计

#sword {
    position: absolute;
    font-size: 80px;
    pointer-events: none;
    z-index: 100;
    transform-origin: center center;
    transition: transform 0.1s ease-out;
    display: none;
}

#sword.slicing {
    transform: rotate(45deg) scale(1.2);
    transition: transform 0.15s ease-in;
}

交互逻辑:

  • 默认隐藏,游戏开始后跟随鼠标移动
  • 切割时添加.slicing类,触发旋转和放大效果
  • pointer-events: none确保不干扰Canvas交互
  • z-index: 100确保在最顶层显示

📚 四、JavaScript核心逻辑分析

📘 4.1 游戏状态管理系统

const gameState = {
    gameRunning: false,
    gamePaused: false,
    gameOver: false
};

function getCurrentGameState() {
    return gameState;
}

状态转换流程:

开始界面 → 点击开始 → gameRunning=true
    ↓
游戏进行中 → 时间耗尽 → gameOver=true
    ↓
结束界面 → 点击重新开始 → 重置状态

应用场景:

  • 控制水果生成定时器
  • 控制游戏循环执行
  • 控制背景音乐播放
  • 控制用户交互响应

📘 4.2 音频系统设计

📖 4.2.1 Web Audio API架构
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const masterGain = audioContext.createGain();
masterGain.connect(audioContext.destination);
masterGain.gain.value = 0.5;

const backgroundMusicGain = audioContext.createGain();
backgroundMusicGain.connect(masterGain);
backgroundMusicGain.gain.value = 0.25;

const soundEffectGain = audioContext.createGain();
soundEffectGain.connect(masterGain);
soundEffectGain.gain.value = 0.5;

音频节点图:

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│  振荡器(osc)│────→│  增益节点    │────→│  主增益     │────→ 扬声器
└─────────────┘     │  (GainNode)  │     │  (master)   │
                    └──────────────┘     └─────────────┘
                         ↑                    ↑
              ┌──────────┴──────────┐          │
              │                     │          │
     ┌──────────────┐     ┌──────────────┐     │
     │ 背景音乐增益  │     │  音效增益    │     │
     │  (0.25)      │     │  (0.5)       │     │
     └──────────────┘     └──────────────┘     │
              │                     │          │
              └──────────┬──────────┘          │
                         │                     │
                    ┌────┴────┐          ┌─────┴─────┐
                    │ 音乐振荡器│          │ 音效振荡器│
                    └─────────┘          └───────────┘
📖 4.2.2 音效生成函数

切割音效(slice):

case 'slice':
    osc = audioContext.createOscillator();
    gain = audioContext.createGain();
    osc.connect(gain);
    gain.connect(soundEffectGain);
    osc.type = 'square';
    osc.frequency.setValueAtTime(800, currentTime);
    osc.frequency.exponentialRampToValueAtTime(1200, currentTime + 0.05);
    osc.frequency.exponentialRampToValueAtTime(400, currentTime + 0.15);
    gain.gain.setValueAtTime(0.4, currentTime);
    gain.gain.exponentialRampToValueAtTime(0.01, currentTime + 0.2);
    osc.start(currentTime);
    osc.stop(currentTime + 0.2);
    break;

音效特点:

  • 方波振荡器(square):音色明亮
  • 频率快速上升后下降:模拟切割的清脆声
  • 0.2秒持续时间:短促有力

炸弹音效(bomb):

case 'bomb':
    osc = audioContext.createOscillator();
    gain = audioContext.createGain();
    osc.connect(gain);
    gain.connect(soundEffectGain);
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(150, currentTime);
    osc.frequency.exponentialRampToValueAtTime(80, currentTime + 0.3);
    gain.gain.setValueAtTime(0.5, currentTime);
    gain.gain.exponentialRampToValueAtTime(0.01, currentTime + 0.3);
    osc.start(currentTime);
    osc.stop(currentTime + 0.3);
    break;

音效特点:

  • 锯齿波振荡器(sawtooth):音色低沉
  • 频率缓慢下降:模拟爆炸的轰鸣声
  • 0.3秒持续时间:更有冲击力

📘 4.3 水果系统设计

📖 4.3.1 水果类型定义
const fruitTypes = [
    { emoji: '🍎', points: 10, speed: 3.5, color: '#FF6B6B' },
    { emoji: '🍌', points: 15, speed: 4, color: '#FFD93D' },
    { emoji: '🍊', points: 12, speed: 3.8, color: '#FF8C42' },
    { emoji: '🍇', points: 20, speed: 4.5, color: '#9B59B6' },
    { emoji: '🍓', points: 18, speed: 4.2, color: '#E74C3C' },
    { emoji: '🍉', points: 25, speed: 3, color: '#27AE60' },
    { emoji: '🍑', points: 15, speed: 3.9, color: '#F39C12' },
    { emoji: '🥝', points: 22, speed: 4.3, color: '#2ECC71' },
    { emoji: '💣', points: -50, speed: 3.2, color: '#34495E', isBomb: true }
];

设计特点:

  • 8种水果 + 1种炸弹
  • 分数范围:10-25分
  • 速度越快分数越高(挑战性更高)
  • 炸弹扣分50分,增加游戏风险
📖 4.3.2 水果生成逻辑
function createFruit() {
    if (!gameState.gameRunning || gameState.gameOver) return;

    const fruitType = fruitTypes[Math.floor(Math.random() * fruitTypes.length)];
    const maxHeight = canvas.height * 0.25;
    const distance = canvas.height - maxHeight;
    const initialSpeed = -Math.sqrt(2 * 0.2 * distance) - (fruitType.speed * 0.5);
    
    const fruit = {
        x: Math.random() * (canvas.width - 100) + 50,
        y: canvas.height,
        vx: (Math.random() - 0.5) * 2,
        vy: initialSpeed,
        prevX: 0,
        prevY: 0,
        type: fruitType,
        emoji: fruitType.emoji,
        points: fruitType.points,
        size: 60,
        rotation: Math.random() * 360,
        rotationSpeed: (Math.random() - 0.5) * 10,
        sliced: false
    };
    fruits.push(fruit);
}

物理公式解析:

  • 使用物理公式 v = √(2gh) 计算初始速度
  • 确保水果能飞到画布1/4高度
  • 添加随机水平速度(vx),使运动轨迹更自然
  • 重力加速度:0.2像素/帧²
📖 4.3.3 水果更新与渲染
function updateFruits() {
    for (let i = fruits.length - 1; i >= 0; i--) {
        const fruit = fruits[i];
        
        fruit.prevX = fruit.x;
        fruit.prevY = fruit.y;
        
        fruit.x += fruit.vx;
        fruit.y += fruit.vy;
        fruit.rotation += fruit.rotationSpeed;
        fruit.vy += 0.2; // 重力

        if (fruit.y > canvas.height + 100 || fruit.x < -50 || fruit.x > canvas.width + 50) {
            fruits.splice(i, 1);
        }
    }
}

function drawFruit(fruit) {
    ctx.save();
    ctx.translate(fruit.x, fruit.y);
    ctx.rotate(fruit.rotation * Math.PI / 180);
    ctx.font = `${fruit.size}px Arial`;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(fruit.emoji, 0, 0);
    ctx.restore();
}

渲染技巧:

  • 使用ctx.save()和ctx.restore()隔离变换状态
  • ctx.translate()将原点移到水果中心
  • ctx.rotate()实现旋转效果
  • Emoji字符直接作为图形渲染

📘 4.4 碰撞检测系统

📖 4.4.1 点到线段距离算法
function pointToLineDistance(px, py, x1, y1, x2, y2) {
    const A = px - x1;
    const B = py - y1;
    const C = x2 - x1;
    const D = y2 - y1;

    const dot = A * C + B * D;
    const lenSq = C * C + D * D;
    let param = -1;

    if (lenSq !== 0) {
        param = dot / lenSq;
    }

    let xx, yy;

    if (param < 0) {
        xx = x1;
        yy = y1;
    } else if (param > 1) {
        xx = x2;
        yy = y2;
    } else {
        xx = x1 + param * C;
        yy = y1 + param * D;
    }

    const dx = px - xx;
    const dy = py - yy;
    return Math.sqrt(dx * dx + dy * dy);
}

算法原理:

  1. 计算点在直线上的投影点
  2. 判断投影点是否在线段范围内
  3. 计算点到投影点的距离
📖 4.4.2 切割检测算法
function checkSlice(x1, y1, x2, y2) {
    const dx = x2 - x1;
    const dy = y2 - y1;
    const dist = Math.sqrt(dx * dx + dy * dy);
    
    if (dist < 5) {
        return checkPointOnFruit(x2, y2);
    }
    
    for (let i = fruits.length - 1; i >= 0; i--) {
        const fruit = fruits[i];
        if (fruit.sliced) continue;

        const fruitDist = pointToLineDistance(fruit.x, fruit.y, x1, y1, x2, y2);
        const prevFruitDist = pointToLineDistance(fruit.prevX, fruit.prevY, x1, y1, x2, y2);
        const pathDist = lineToLineDistance(fruit.prevX, fruit.prevY, fruit.x, fruit.y, x1, y1, x2, y2);
        
        let pathPointHit = false;
        const steps = Math.max(10, Math.floor(dist / 10));
        for (let t = 0; t <= 1; t += 1 / steps) {
            const px = x1 + t * (x2 - x1);
            const py = y1 + t * (y2 - y1);
            const dx2 = fruit.x - px;
            const dy2 = fruit.y - py;
            const pointDist = Math.sqrt(dx2 * dx2 + dy2 * dy2);
            if (pointDist < fruit.size / 2) {
                pathPointHit = true;
                break;
            }
        }
        
        if (fruitDist < fruit.size / 2 || 
            prevFruitDist < fruit.size / 2 || 
            pathDist < fruit.size / 2 ||
            pathPointHit) {
            sliceFruit(i);
            return true;
        }
    }
    return false;
}

多重检测策略:

  1. 当前位置检测:水果中心到切割线段的距离
  2. 上一帧位置检测:处理快速移动的水果
  3. 路径距离检测:水果移动路径与切割路径的最短距离
  4. 密集采样检测:在切割路径上采样多个点检测

优势:

  • 提高切割检测的准确性
  • 减少漏切和误判
  • 支持快速滑动切割

📘 4.5 切割逻辑与得分系统

function sliceFruit(index) {
    const fruit = fruits[index];
    if (fruit.sliced) return;

    fruit.sliced = true;

    const currentTime = Date.now();
    const timeSinceLastSlice = currentTime - lastSliceTime;
    
    if (timeSinceLastSlice < 2000) {
        comboCount++;
    } else {
        comboCount = 1;
    }
    
    lastSliceTime = currentTime;

    let points = fruit.points;
    if (comboCount >= 10) {
        points = Math.floor(points * 3);
    } else if (comboCount >= 5) {
        points = Math.floor(points * 2);
    }

    score += points;
    document.getElementById('score').textContent = score;

    if (fruit.type.isBomb) {
        playSound('bomb');
        createEffect('💥', fruit.x, fruit.y);
        createComboPopup('💣 炸弹!', fruit.x, fruit.y - 50);
    } else if (comboCount >= 10) {
        playSound('combo');
        createComboPopup(`🔥 ${comboCount}连击!`, fruit.x, fruit.y - 50);
    } else if (comboCount >= 5) {
        playSound('combo');
        createComboPopup(`${comboCount}连击!`, fruit.x, fruit.y - 50);
    } else {
        playSound('slice');
    }

    const effects = ['⭐', '✨', '💫', '🌟', '🎯', '💥', '🎉'];
    const randomEffect = effects[Math.floor(Math.random() * effects.length)];
    createEffect(randomEffect, fruit.x, fruit.y);
    createScorePopup(points, fruit.x, fruit.y - 30);

    fruits.splice(index, 1);
}

连击系统设计:

  • 2秒内连续切割触发连击
  • 5连击:分数x2
  • 10连击:分数x3
  • 不同连击数播放不同音效和提示

视觉反馈:

  • 切割时显示随机特效
  • 显示得分弹出
  • 连击时显示连击提示

📘 4.6 游戏循环与渲染

function gameLoop() {
    if (!gameState.gameRunning || gameState.gameOver) return;

    ctx.clearRect(0, 0, canvas.width, canvas.height);

    const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
    gradient.addColorStop(0, '#87CEEB');
    gradient.addColorStop(0.5, '#98D8C8');
    gradient.addColorStop(1, '#F7DC6F');
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    updateFruits();
    
    fruits.forEach(fruit => {
        if (!fruit.sliced) {
            drawFruit(fruit);
        }
    });

    // 绘制切割轨迹
    if (slicePath.length > 1) {
        ctx.beginPath();
        ctx.moveTo(slicePath[0].x, slicePath[0].y);
        for (let i = 1; i < slicePath.length; i++) {
            ctx.lineTo(slicePath[i].x, slicePath[i].y);
        }
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
        ctx.lineWidth = 3;
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';
        ctx.stroke();
    }

    requestAnimationFrame(gameLoop);
}

渲染流程:

  1. 清空画布
  2. 绘制背景渐变
  3. 更新水果位置(物理模拟)
  4. 绘制所有未切割的水果
  5. 绘制切割轨迹
  6. 请求下一帧

性能优化:

  • 使用requestAnimationFrame确保60fps
  • 只渲染未切割的水果
  • 切割轨迹使用低透明度减少性能影响

📘 4.7 用户交互系统

📖 4.7.1 鼠标事件处理
let lastMouseX = 0;
let lastMouseY = 0;

// 鼠标按下
canvas.addEventListener('mousedown', (e) => {
    if (!gameState.gameRunning || gameState.gameOver) return;
    
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    
    mouseX = x;
    mouseY = y;
    lastMouseX = x;
    lastMouseY = y;
    isSlicing = true;
    slicePath = [{ x, y }];
    
    sword.style.display = 'block';
    sword.style.left = (x - 40) + 'px';
    sword.style.top = (y - 40) + 'px';
    sword.classList.add('slicing');
    
    checkPointOnFruit(x, y);
});

// 鼠标移动
canvas.addEventListener('mousemove', (e) => {
    if (!gameState.gameRunning || gameState.gameOver) return;
    
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    
    mouseX = x;
    mouseY = y;
    
    sword.style.left = (x - 40) + 'px';
    sword.style.top = (y - 40) + 'px';
    
    if (isSlicing) {
        slicePath.push({ x, y });
        
        if (slicePath.length > 1) {
            const lastPoint = slicePath[slicePath.length - 2];
            checkSlice(lastPoint.x, lastPoint.y, x, y);
        }
        
        if (slicePath.length > 20) {
            slicePath.shift();
        }
    }
});

// 鼠标释放
canvas.addEventListener('mouseup', () => {
    isSlicing = false;
    sword.classList.remove('slicing');
    slicePath = [];
});

交互逻辑:

  • 鼠标按下:开始切割,记录起点
  • 鼠标移动:更新切割轨迹,检测碰撞
  • 鼠标释放:结束切割,清除轨迹

切割轨迹优化:

  • 最多保存20个点,防止内存溢出
  • 每移动一段距离检测一次碰撞
  • 轨迹使用白色半透明线条
📖 4.7.2 触摸事件处理
canvas.addEventListener('touchstart', (e) => {
    if (!gameState.gameRunning || gameState.gameOver) return;
    e.preventDefault();
    
    const touch = e.touches[0];
    const rect = canvas.getBoundingClientRect();
    const x = touch.clientX - rect.left;
    const y = touch.clientY - rect.top;
    
    mouseX = x;
    mouseY = y;
    lastMouseX = x;
    lastMouseY = y;
    isSlicing = true;
    slicePath = [{ x, y }];
    
    sword.style.display = 'block';
    sword.style.left = (x - 40) + 'px';
    sword.style.top = (y - 40) + 'px';
    sword.classList.add('slicing');
    
    checkPointOnFruit(x, y);
});

canvas.addEventListener('touchmove', (e) => {
    if (!gameState.gameRunning || gameState.gameOver) return;
    e.preventDefault();
    
    const touch = e.touches[0];
    const rect = canvas.getBoundingClientRect();
    const x = touch.clientX - rect.left;
    const y = touch.clientY - rect.top;
    
    mouseX = x;
    mouseY = y;
    
    sword.style.left = (x - 40) + 'px';
    sword.style.top = (y - 40) + 'px';
    
    if (isSlicing) {
        slicePath.push({ x, y });
        
        if (slicePath.length > 1) {
            const lastPoint = slicePath[slicePath.length - 2];
            checkSlice(lastPoint.x, lastPoint.y, x, y);
        }
        
        if (slicePath.length > 20) {
            slicePath.shift();
        }
    }
});

canvas.addEventListener('touchend', () => {
    isSlicing = false;
    sword.classList.remove('slicing');
    slicePath = [];
});

移动端适配:

  • 支持触摸事件
  • e.preventDefault()防止页面滚动
  • 单触摸点检测

📘 4.8 游戏初始化与结束

function initGame() {
    score = 0;
    timeLeft = 60;
    comboCount = 0;
    lastSliceTime = 0;
    fruits.length = 0;
    gameState.gameRunning = true;
    gameState.gamePaused = false;
    gameState.gameOver = false;
    
    document.getElementById('score').textContent = score;
    document.getElementById('time').textContent = timeLeft;
    document.getElementById('startScreen').style.display = 'none';
    document.getElementById('gameOverScreen').style.display = 'none';

    gameTimer = setInterval(() => {
        timeLeft--;
        document.getElementById('time').textContent = timeLeft;
        
        if (timeLeft <= 0) {
            gameOver();
        }
    }, 1000);

    fruitTimer = setInterval(() => {
        if (gameState.gameRunning && !gameState.gameOver) {
            createFruit();
        }
    }, 800 + Math.random() * 600);

    setTimeout(() => createFruit(), 500);

    startBackgroundMusic();
    gameLoop();
}

function gameOver() {
    gameState.gameOver = true;
    gameState.gameRunning = false;
    stopBackgroundMusic();

    if (gameTimer) {
        clearInterval(gameTimer);
        gameTimer = null;
    }
    if (fruitTimer) {
        clearInterval(fruitTimer);
        fruitTimer = null;
    }

    if (score > highScore) {
        highScore = score;
        localStorage.setItem('fruitNinjaHighScore', highScore);
        document.getElementById('highScore').textContent = highScore;
    }

    document.getElementById('finalScore').textContent = `得分: ${score}`;
    document.getElementById('gameOverScreen').style.display = 'flex';
}

初始化流程:

  1. 重置所有游戏变量
  2. 隐藏开始界面
  3. 启动倒计时定时器(1秒/次)
  4. 启动水果生成定时器(0.8-1.4秒/次)
  5. 延迟500ms生成第一个水果
  6. 开始背景音乐
  7. 启动游戏循环

结束流程:

  1. 更新游戏状态
  2. 停止背景音乐
  3. 清除所有定时器
  4. 更新最高分(如果破纪录)
  5. 显示结束界面

📚 五、核心算法与技术亮点

📘 5.1 物理引擎实现

抛物线运动模拟:

位置更新:
  x = x + vx
  y = y + vy
  vy = vy + gravity (0.2)

初始速度计算:
  v = -√(2 * g * h)
  其中g=0.2, h=目标高度

效果:

  • 水果从底部弹出,到达最高点后下落
  • 符合真实物理规律
  • 视觉效果自然流畅

📘 5.2 碰撞检测优化

多重检测策略:

  1. 点到线段距离
  2. 线段到线段距离
  3. 密集采样检测
  4. 历史位置检测

优势:

  • 准确率高(>95%)
  • 响应速度快(<16ms)
  • 支持快速滑动
  • 减少误判和漏判

📘 5.3 音效合成技术

Web Audio API应用:

  • 振荡器类型:正弦波、方波、锯齿波
  • 频率调制:exponentialRampToValueAtTime
  • 音量控制:gain节点
  • 多音合成:琶音效果

音效设计:

  • 切割音效:方波 + 频率快速变化
  • 炸弹音效:锯齿波 + 频率缓慢下降
  • 连击音效:多振荡器 + 延迟触发

📘 5.4 性能优化策略

渲染优化:

  • 只渲染可见元素
  • 使用requestAnimationFrame
  • 限制切割轨迹点数(20个)
  • Canvas离屏渲染(可选)

内存优化:

  • 及时移除超出边界的水果
  • 限制数组大小
  • 避免内存泄漏

交互优化:

  • 事件节流(throttle)
  • 碰撞检测优化
  • 减少DOM操作

📚 六、代码质量与可维护性

📘 6.1 代码结构分析

优点:

  • 函数职责单一
  • 变量命名清晰
  • 注释详细
  • 逻辑流程清晰

可改进之处:

  • 单文件架构,代码量较大(约1500行)
  • 缺乏模块化设计
  • 硬编码较多(如水果类型、颜色、速度等)
  • 缺少错误处理

📘 6.2 设计模式应用

状态模式:

  • 游戏状态管理
  • 状态转换清晰

工厂模式:

  • 水果创建函数
  • 特效创建函数

观察者模式:

  • 事件监听系统
  • 状态变化通知

📘 6.3 可扩展性分析

易于扩展的功能:

  • 新增水果类型
  • 新增特效类型
  • 调整游戏参数

难以扩展的功能:

  • 游戏模式(如无尽模式、挑战模式)
  • 多人对战
  • 复杂道具系统

📚 七、总结与展望

📘 7.1 项目亮点

  1. 完整的游戏体验: 包含开始界面、游戏过程、结束界面的完整流程
  2. 流畅的交互体验: 60fps渲染,响应迅速
  3. 丰富的视觉效果: 特效动画、得分弹出、连击提示
  4. 真实的物理模拟: 抛物线运动、重力加速度
  5. 智能的碰撞检测: 多重检测策略,准确率高
  6. 沉浸式音效系统: Web Audio API合成音效

📘 7.2 技术价值

学习价值:

  • Canvas 2D绘图技术
  • Web Audio API音频合成
  • 物理引擎基础
  • 碰撞检测算法
  • 游戏循环设计
  • 性能优化技巧

应用价值:

  • 休闲游戏开发
  • H5小游戏制作
  • 移动端适配
  • 交互设计参考

📘 7.3 改进建议

短期改进:

  • 增加暂停功能
  • 调整难度曲线(随时间增加难度)
  • 增加更多水果类型
  • 添加成就系统

中期改进:

  • 重构为模块化架构
  • 增加更多游戏模式
  • 添加道具系统
  • 优化移动端体验

长期改进:

  • 支持多人对战
  • 添加社交分享功能
  • 实现云端排行榜
  • 开发关卡系统

📚 完整代码(可直接使用)

📘 项目目录

项目目录

📘 项目代码

📖 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>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Arial', sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            background: linear-gradient(135deg, #FF6B6B 0%, #4ECDC4 50%, #45B7D1 100%);
            overflow: hidden;
        }

        #gameContainer {
            position: relative;
            width: 900px;
            height: 700px;
            background: linear-gradient(to bottom, #87CEEB 0%, #98D8C8 50%, #F7DC6F 100%);
            border: 5px solid #fff;
            border-radius: 20px;
            box-shadow: 0 15px 40px rgba(0, 0, 0, 0.5);
            overflow: hidden;
            cursor: none;
        }

        #gameCanvas {
            display: block;
            width: 100%;
            height: 100%;
        }

        #ui {
            position: absolute;
            top: 20px;
            left: 20px;
            right: 20px;
            display: flex;
            justify-content: space-between;
            color: #fff;
            font-size: 24px;
            font-weight: bold;
            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
            z-index: 10;
            pointer-events: none;
        }

        #startScreen {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.8);
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            color: #fff;
            z-index: 100;
            cursor: default;
        }

        #startScreen h1 {
            font-size: 56px;
            margin-bottom: 20px;
            text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.8);
        }

        #startScreen p {
            font-size: 22px;
            margin-bottom: 30px;
            text-align: center;
        }

        #startButton {
            padding: 15px 40px;
            font-size: 24px;
            background: #4CAF50;
            color: #fff;
            border: none;
            border-radius: 10px;
            cursor: pointer;
            transition: all 0.3s;
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
        }

        #startButton:hover {
            background: #45a049;
            transform: scale(1.05);
        }

        #gameOverScreen {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.8);
            display: none;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            color: #fff;
            z-index: 100;
            cursor: default;
        }

        #gameOverScreen h2 {
            font-size: 48px;
            margin-bottom: 20px;
            color: #ff4444;
        }

        #finalScore {
            font-size: 32px;
            margin-bottom: 30px;
        }

        #restartButton {
            padding: 15px 40px;
            font-size: 24px;
            background: #4CAF50;
            color: #fff;
            border: none;
            border-radius: 10px;
            cursor: pointer;
            transition: all 0.3s;
        }

        #restartButton:hover {
            background: #45a049;
            transform: scale(1.05);
        }

        .effect {
            position: absolute;
            font-size: 60px;
            pointer-events: none;
            z-index: 50;
            animation: floatUp 1.2s ease-out forwards;
            user-select: none;
        }

        @keyframes floatUp {
            0% {
                opacity: 1;
                transform: translateY(0) scale(1) rotate(0deg);
            }
            100% {
                opacity: 0;
                transform: translateY(-200px) scale(1.8) rotate(360deg);
            }
        }

        .score-popup {
            position: absolute;
            font-size: 36px;
            font-weight: bold;
            color: #FFD700;
            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
            pointer-events: none;
            z-index: 60;
            animation: scoreFloat 1s ease-out forwards;
        }

        @keyframes scoreFloat {
            0% {
                opacity: 1;
                transform: translateY(0) scale(1);
            }
            100% {
                opacity: 0;
                transform: translateY(-120px) scale(1.5);
            }
        }

        .combo-popup {
            position: absolute;
            font-size: 48px;
            font-weight: bold;
            color: #FF6B6B;
            text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.8);
            pointer-events: none;
            z-index: 70;
            animation: comboFloat 1.5s ease-out forwards;
        }

        @keyframes comboFloat {
            0% {
                opacity: 1;
                transform: translateY(0) scale(0.5);
            }
            50% {
                opacity: 1;
                transform: translateY(-50px) scale(1.2);
            }
            100% {
                opacity: 0;
                transform: translateY(-150px) scale(1);
            }
        }

        #sword {
            position: absolute;
            font-size: 80px;
            pointer-events: none;
            z-index: 100;
            transform-origin: center center;
            transition: transform 0.1s ease-out;
            user-select: none;
            display: none;
        }

        #sword.slicing {
            transform: rotate(45deg) scale(1.2);
            transition: transform 0.15s ease-in;
        }

        .fruit {
            position: absolute;
            font-size: 60px;
            pointer-events: none;
            user-select: none;
            transition: transform 0.1s;
        }

        .fruit-sliced {
            opacity: 0.6;
            pointer-events: none;
        }
    </style>
</head>
<body>
    <div id="gameContainer">
        <canvas id="gameCanvas"></canvas>
        <div id="ui">
            <div>得分: <span id="score">0</span></div>
            <div>时间: <span id="time">60</span>秒</div>
            <div>最高分: <span id="highScore">0</span></div>
        </div>
        <div id="sword">🗡️</div>
        <div id="startScreen">
            <h1>🍎 切水果游戏 🍎</h1>
            <p>用鼠标或手指滑动来切水果!</p>
            <p>游戏时间:60秒</p>
            <p>切得越多,得分越高!</p>
            <button id="startButton">开始游戏</button>
        </div>
        <div id="gameOverScreen">
            <h2>游戏结束</h2>
            <div id="finalScore">得分: 0</div>
            <button id="restartButton">重新开始</button>
        </div>
    </div>

    <script src="backgroundMusic.js"></script>
    <script>
        // 游戏状态
        const gameState = {
            gameRunning: false,
            gamePaused: false,
            gameOver: false
        };

        // 获取游戏状态(供backgroundMusic.js使用)
        function getCurrentGameState() {
            return gameState;
        }

        // 初始化音频上下文
        const audioContext = new (window.AudioContext || window.webkitAudioContext)();
        const masterGain = audioContext.createGain();
        masterGain.connect(audioContext.destination);
        masterGain.gain.value = 0.5;

        const backgroundMusicGain = audioContext.createGain();
        backgroundMusicGain.connect(masterGain);
        backgroundMusicGain.gain.value = 0.25;

        const soundEffectGain = audioContext.createGain();
        soundEffectGain.connect(masterGain);
        soundEffectGain.gain.value = 0.5;

        // 初始化背景音乐
        initBackgroundMusic(audioContext, backgroundMusicGain);
        setGameStateGetter(getCurrentGameState);

        // 音效生成函数
        function playSound(type) {
            const currentTime = audioContext.currentTime;
            let osc, gain;

            switch(type) {
                case 'slice':
                    // 切水果音效 - 清脆的切割声
                    osc = audioContext.createOscillator();
                    gain = audioContext.createGain();
                    osc.connect(gain);
                    gain.connect(soundEffectGain);
                    osc.type = 'square';
                    osc.frequency.setValueAtTime(800, currentTime);
                    osc.frequency.exponentialRampToValueAtTime(1200, currentTime + 0.05);
                    osc.frequency.exponentialRampToValueAtTime(400, currentTime + 0.15);
                    gain.gain.setValueAtTime(0.4, currentTime);
                    gain.gain.exponentialRampToValueAtTime(0.01, currentTime + 0.2);
                    osc.start(currentTime);
                    osc.stop(currentTime + 0.2);
                    break;

                case 'miss':
                    // 未切中音效 - 低沉的提示音
                    osc = audioContext.createOscillator();
                    gain = audioContext.createGain();
                    osc.connect(gain);
                    gain.connect(soundEffectGain);
                    osc.type = 'sine';
                    osc.frequency.setValueAtTime(200, currentTime);
                    osc.frequency.exponentialRampToValueAtTime(150, currentTime + 0.1);
                    gain.gain.setValueAtTime(0.15, currentTime);
                    gain.gain.exponentialRampToValueAtTime(0.01, currentTime + 0.1);
                    osc.start(currentTime);
                    osc.stop(currentTime + 0.1);
                    break;

                case 'combo':
                    // 连击音效 - 上升的琶音
                    for (let i = 0; i < 4; i++) {
                        const delay = i * 0.06;
                        osc = audioContext.createOscillator();
                        gain = audioContext.createGain();
                        osc.connect(gain);
                        gain.connect(soundEffectGain);
                        osc.type = 'sine';
                        osc.frequency.value = 600 + i * 300;
                        gain.gain.setValueAtTime(0.3, currentTime + delay);
                        gain.gain.exponentialRampToValueAtTime(0.01, currentTime + delay + 0.25);
                        osc.start(currentTime + delay);
                        osc.stop(currentTime + delay + 0.25);
                    }
                    break;

                case 'bomb':
                    // 炸弹音效 - 低沉的爆炸声
                    osc = audioContext.createOscillator();
                    gain = audioContext.createGain();
                    osc.connect(gain);
                    gain.connect(soundEffectGain);
                    osc.type = 'sawtooth';
                    osc.frequency.setValueAtTime(150, currentTime);
                    osc.frequency.exponentialRampToValueAtTime(80, currentTime + 0.3);
                    gain.gain.setValueAtTime(0.5, currentTime);
                    gain.gain.exponentialRampToValueAtTime(0.01, currentTime + 0.3);
                    osc.start(currentTime);
                    osc.stop(currentTime + 0.3);
                    break;
            }
        }

        // 创建特效函数
        function createEffect(emoji, x, y) {
            const effect = document.createElement('div');
            effect.className = 'effect';
            effect.textContent = emoji;
            effect.style.left = x + 'px';
            effect.style.top = y + 'px';
            document.getElementById('gameContainer').appendChild(effect);
            
            setTimeout(() => {
                effect.remove();
            }, 1200);
        }

        // 创建得分弹出效果
        function createScorePopup(score, x, y) {
            const popup = document.createElement('div');
            popup.className = 'score-popup';
            popup.textContent = '+' + score;
            popup.style.left = x + 'px';
            popup.style.top = y + 'px';
            document.getElementById('gameContainer').appendChild(popup);
            
            setTimeout(() => {
                popup.remove();
            }, 1000);
        }

        // 创建连击弹出效果
        function createComboPopup(text, x, y) {
            const popup = document.createElement('div');
            popup.className = 'combo-popup';
            popup.textContent = text;
            popup.style.left = x + 'px';
            popup.style.top = y + 'px';
            document.getElementById('gameContainer').appendChild(popup);
            
            setTimeout(() => {
                popup.remove();
            }, 1500);
        }

        // 游戏变量
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const gameContainer = document.getElementById('gameContainer');
        const sword = document.getElementById('sword');
        
        canvas.width = gameContainer.offsetWidth;
        canvas.height = gameContainer.offsetHeight;

        const fruits = [];
        const slicedFruits = [];
        let score = 0;
        let highScore = localStorage.getItem('fruitNinjaHighScore') || 0;
        let timeLeft = 60;
        let gameTimer = null;
        let fruitTimer = null;
        let comboCount = 0;
        let lastSliceTime = 0;
        let mouseX = 0;
        let mouseY = 0;
        let isSlicing = false;
        let slicePath = [];

        // 水果类型
        const fruitTypes = [
            { emoji: '🍎', points: 10, speed: 3.5, color: '#FF6B6B' },
            { emoji: '🍌', points: 15, speed: 4, color: '#FFD93D' },
            { emoji: '🍊', points: 12, speed: 3.8, color: '#FF8C42' },
            { emoji: '🍇', points: 20, speed: 4.5, color: '#9B59B6' },
            { emoji: '🍓', points: 18, speed: 4.2, color: '#E74C3C' },
            { emoji: '🍉', points: 25, speed: 3, color: '#27AE60' },
            { emoji: '🍑', points: 15, speed: 3.9, color: '#F39C12' },
            { emoji: '🥝', points: 22, speed: 4.3, color: '#2ECC71' },
            { emoji: '💣', points: -50, speed: 3.2, color: '#34495E', isBomb: true }
        ];

        document.getElementById('highScore').textContent = highScore;

        // 创建水果
        function createFruit() {
            if (!gameState.gameRunning || gameState.gameOver) return;

            const fruitType = fruitTypes[Math.floor(Math.random() * fruitTypes.length)];
            // 计算目标高度(画布的1/4处,作为最高点)
            const maxHeight = canvas.height * 0.25;
            // 计算初始向上速度,让水果能飞到最高点
            const distance = canvas.height - maxHeight;
            // 使用物理公式:v^2 = 2gh,计算初始速度
            const initialSpeed = -Math.sqrt(2 * 0.2 * distance) - (fruitType.speed * 0.5);
            
            const fruit = {
                x: Math.random() * (canvas.width - 100) + 50,
                y: canvas.height, // 从底部开始
                vx: (Math.random() - 0.5) * 2,
                vy: initialSpeed, // 向上飞行的速度
                prevX: 0, // 上一帧的x位置
                prevY: 0, // 上一帧的y位置
                type: fruitType,
                emoji: fruitType.emoji,
                points: fruitType.points,
                size: 60,
                rotation: Math.random() * 360,
                rotationSpeed: (Math.random() - 0.5) * 10,
                sliced: false
            };
            fruits.push(fruit);
        }

        // 绘制水果
        function drawFruit(fruit) {
            ctx.save();
            ctx.translate(fruit.x, fruit.y);
            ctx.rotate(fruit.rotation * Math.PI / 180);
            ctx.font = `${fruit.size}px Arial`;
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            ctx.fillText(fruit.emoji, 0, 0);
            ctx.restore();
        }

        // 更新水果位置
        function updateFruits() {
            for (let i = fruits.length - 1; i >= 0; i--) {
                const fruit = fruits[i];
                
                // 保存上一帧位置
                fruit.prevX = fruit.x;
                fruit.prevY = fruit.y;
                
                fruit.x += fruit.vx;
                fruit.y += fruit.vy;
                fruit.rotation += fruit.rotationSpeed;
                // 重力,让水果有抛物线运动
                fruit.vy += 0.2;

                // 只在水果掉出画布底部或左右边界时才移除
                if (fruit.y > canvas.height + 100 || fruit.x < -50 || fruit.x > canvas.width + 50) {
                    fruits.splice(i, 1);
                }
            }
        }

        // 检测点是否在水果上(用于鼠标按下时的立即检测)
        function checkPointOnFruit(x, y) {
            for (let i = fruits.length - 1; i >= 0; i--) {
                const fruit = fruits[i];
                if (fruit.sliced) continue;

                // 计算点到水果中心的距离
                const dx = fruit.x - x;
                const dy = fruit.y - y;
                const dist = Math.sqrt(dx * dx + dy * dy);
                
                if (dist < fruit.size / 2) {
                    sliceFruit(i);
                    return true;
                }
            }
            return false;
        }

        // 检测切割(改进版:考虑水果的移动路径)
        function checkSlice(x1, y1, x2, y2) {
            // 如果起点和终点相同或非常接近,使用点检测
            const dx = x2 - x1;
            const dy = y2 - y1;
            const dist = Math.sqrt(dx * dx + dy * dy);
            
            if (dist < 5) {
                // 距离太近,使用点检测
                return checkPointOnFruit(x2, y2);
            }
            
            for (let i = fruits.length - 1; i >= 0; i--) {
                const fruit = fruits[i];
                if (fruit.sliced) continue;

                // 方法1:检测水果当前位置到线段的距离
                const fruitDist = pointToLineDistance(fruit.x, fruit.y, x1, y1, x2, y2);
                
                // 方法2:检测水果上一帧位置到线段的距离(考虑快速移动)
                const prevFruitDist = pointToLineDistance(fruit.prevX, fruit.prevY, x1, y1, x2, y2);
                
                // 方法3:检测水果移动路径与切割路径是否相交
                // 计算水果移动路径与切割路径的最短距离
                const pathDist = lineToLineDistance(
                    fruit.prevX, fruit.prevY, fruit.x, fruit.y,
                    x1, y1, x2, y2
                );
                
                // 方法4:检测切割路径上的点是否经过水果(更密集的采样)
                let pathPointHit = false;
                const steps = Math.max(10, Math.floor(dist / 10)); // 根据距离调整采样点数
                for (let t = 0; t <= 1; t += 1 / steps) {
                    const px = x1 + t * (x2 - x1);
                    const py = y1 + t * (y2 - y1);
                    const dx2 = fruit.x - px;
                    const dy2 = fruit.y - py;
                    const pointDist = Math.sqrt(dx2 * dx2 + dy2 * dy2);
                    if (pointDist < fruit.size / 2) {
                        pathPointHit = true;
                        break;
                    }
                }
                
                // 如果任一方法检测到碰撞,就切水果
                if (fruitDist < fruit.size / 2 || 
                    prevFruitDist < fruit.size / 2 || 
                    pathDist < fruit.size / 2 ||
                    pathPointHit) {
                    sliceFruit(i);
                    return true;
                }
            }
            return false;
        }

        // 计算两条线段之间的最短距离
        function lineToLineDistance(x1, y1, x2, y2, x3, y3, x4, y4) {
            // 简化:检测两条线段上最近的点对
            let minDist = Infinity;
            
            // 检测线段1上的点到线段2的距离
            for (let t = 0; t <= 1; t += 0.1) {
                const px = x1 + t * (x2 - x1);
                const py = y1 + t * (y2 - y1);
                const dist = pointToLineDistance(px, py, x3, y3, x4, y4);
                minDist = Math.min(minDist, dist);
            }
            
            // 检测线段2上的点到线段1的距离
            for (let t = 0; t <= 1; t += 0.1) {
                const px = x3 + t * (x4 - x3);
                const py = y3 + t * (y4 - y3);
                const dist = pointToLineDistance(px, py, x1, y1, x2, y2);
                minDist = Math.min(minDist, dist);
            }
            
            return minDist;
        }

        // 点到线段距离
        function pointToLineDistance(px, py, x1, y1, x2, y2) {
            const A = px - x1;
            const B = py - y1;
            const C = x2 - x1;
            const D = y2 - y1;

            const dot = A * C + B * D;
            const lenSq = C * C + D * D;
            let param = -1;
            if (lenSq !== 0) param = dot / lenSq;

            let xx, yy;
            if (param < 0) {
                xx = x1;
                yy = y1;
            } else if (param > 1) {
                xx = x2;
                yy = y2;
            } else {
                xx = x1 + param * C;
                yy = y1 + param * D;
            }

            const dx = px - xx;
            const dy = py - yy;
            return Math.sqrt(dx * dx + dy * dy);
        }

        // 切水果
        function sliceFruit(index) {
            const fruit = fruits[index];
            if (fruit.sliced) return;

            fruit.sliced = true;

            // 计算得分
            const currentTime = Date.now();
            const timeSinceLastSlice = currentTime - lastSliceTime;
            
            if (timeSinceLastSlice < 2000) {
                comboCount++;
            } else {
                comboCount = 1;
            }
            
            lastSliceTime = currentTime;

            let points = fruit.points;
            if (comboCount >= 10) {
                points = Math.floor(points * 3);
            } else if (comboCount >= 5) {
                points = Math.floor(points * 2);
            }

            // 炸弹特殊处理
            if (fruit.type.isBomb) {
                playSound('bomb');
                createEffect('💥', fruit.x, fruit.y);
                createComboPopup('💣 炸弹!', fruit.x, fruit.y - 50);
                score = Math.max(0, score + points);
                comboCount = 0;
            } else {
                score += points;
                
                // 播放音效
                if (comboCount >= 10) {
                    playSound('combo');
                    createComboPopup(`🔥 ${comboCount}连击!`, fruit.x, fruit.y - 50);
                } else if (comboCount >= 5) {
                    playSound('combo');
                    createComboPopup(`${comboCount}连击!`, fruit.x, fruit.y - 50);
                } else {
                    playSound('slice');
                }

                // 创建特效
                const effects = ['⭐', '✨', '💫', '🌟', '🎯', '💥'];
                const randomEffect = effects[Math.floor(Math.random() * effects.length)];
                createEffect(randomEffect, fruit.x, fruit.y);
                createScorePopup(points, fruit.x, fruit.y - 30);
            }

            document.getElementById('score').textContent = score;

            // 移除水果
            fruits.splice(index, 1);
        }

        // 鼠标/触摸事件
        let lastMouseX = 0;
        let lastMouseY = 0;

        gameContainer.addEventListener('mousemove', (e) => {
            if (!gameState.gameRunning || gameState.gameOver) return;
            
            const rect = gameContainer.getBoundingClientRect();
            const newMouseX = e.clientX - rect.left;
            const newMouseY = e.clientY - rect.top;
            
            sword.style.left = (newMouseX - 40) + 'px';
            sword.style.top = (newMouseY - 40) + 'px';
            sword.style.display = 'block';

            // 只要按着鼠标左键,就持续检测切割
            if (isSlicing) {
                if (lastMouseX !== 0 || lastMouseY !== 0) {
                    // 有移动路径,检测线段(包括快速移动的情况)
                    checkSlice(lastMouseX, lastMouseY, newMouseX, newMouseY);
                } else {
                    // 刚按下还没移动,检测当前位置
                    checkPointOnFruit(newMouseX, newMouseY);
                }
                // 同时检测新位置的点
                checkPointOnFruit(newMouseX, newMouseY);
            }

            mouseX = newMouseX;
            mouseY = newMouseY;
            lastMouseX = newMouseX;
            lastMouseY = newMouseY;
        });

        gameContainer.addEventListener('mousedown', (e) => {
            if (!gameState.gameRunning || gameState.gameOver) return;
            
            isSlicing = true;
            sword.classList.add('slicing');
            const rect = gameContainer.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;
            lastMouseX = x;
            lastMouseY = y;
            
            // 按下鼠标时立即检测当前位置是否有水果
            checkPointOnFruit(x, y);
        });

        gameContainer.addEventListener('mouseup', () => {
            isSlicing = false;
            sword.classList.remove('slicing');
        });

        gameContainer.addEventListener('mouseleave', () => {
            isSlicing = false;
            sword.classList.remove('slicing');
            sword.style.display = 'none';
            lastMouseX = 0;
            lastMouseY = 0;
        });

        // 触摸事件
        gameContainer.addEventListener('touchmove', (e) => {
            if (!gameState.gameRunning || gameState.gameOver) return;
            e.preventDefault();
            
            const touch = e.touches[0];
            const rect = gameContainer.getBoundingClientRect();
            const newMouseX = touch.clientX - rect.left;
            const newMouseY = touch.clientY - rect.top;
            
            sword.style.left = (newMouseX - 40) + 'px';
            sword.style.top = (newMouseY - 40) + 'px';
            sword.style.display = 'block';

            // 只要触摸着,就持续检测切割
            if (isSlicing) {
                if (lastMouseX !== 0 || lastMouseY !== 0) {
                    // 有移动路径,检测线段(包括快速移动的情况)
                    checkSlice(lastMouseX, lastMouseY, newMouseX, newMouseY);
                } else {
                    // 刚按下还没移动,检测当前位置
                    checkPointOnFruit(newMouseX, newMouseY);
                }
                // 同时检测新位置的点
                checkPointOnFruit(newMouseX, newMouseY);
            }

            mouseX = newMouseX;
            mouseY = newMouseY;
            lastMouseX = newMouseX;
            lastMouseY = newMouseY;
        });

        gameContainer.addEventListener('touchstart', (e) => {
            if (!gameState.gameRunning || gameState.gameOver) return;
            e.preventDefault();
            
            const touch = e.touches[0];
            const rect = gameContainer.getBoundingClientRect();
            isSlicing = true;
            sword.classList.add('slicing');
            const x = touch.clientX - rect.left;
            const y = touch.clientY - rect.top;
            lastMouseX = x;
            lastMouseY = y;
            
            // 触摸开始时立即检测当前位置是否有水果
            checkPointOnFruit(x, y);
        });

        gameContainer.addEventListener('touchend', (e) => {
            e.preventDefault();
            isSlicing = false;
            sword.classList.remove('slicing');
            lastMouseX = 0;
            lastMouseY = 0;
        });

        // 游戏循环
        function gameLoop() {
            if (!gameState.gameRunning || gameState.gameOver) return;

            ctx.clearRect(0, 0, canvas.width, canvas.height);

            updateFruits();
            
            // 如果正在切割,在游戏循环中也持续检测(确保快速移动的水果也能被检测到)
            if (isSlicing && (lastMouseX !== 0 || lastMouseY !== 0)) {
                // 检测当前鼠标位置和水果的碰撞
                checkPointOnFruit(mouseX, mouseY);
                
                // 如果有移动路径,也检测路径
                if (lastMouseX !== mouseX || lastMouseY !== mouseY) {
                    checkSlice(lastMouseX, lastMouseY, mouseX, mouseY);
                }
            }
            
            // 绘制所有水果
            fruits.forEach(fruit => {
                if (!fruit.sliced) {
                    drawFruit(fruit);
                }
            });

            requestAnimationFrame(gameLoop);
        }

        // 初始化游戏
        function initGame() {
            score = 0;
            timeLeft = 60;
            comboCount = 0;
            lastSliceTime = 0;
            fruits.length = 0;
            gameState.gameRunning = true;
            gameState.gamePaused = false;
            gameState.gameOver = false;
            
            document.getElementById('score').textContent = score;
            document.getElementById('time').textContent = timeLeft;
            document.getElementById('startScreen').style.display = 'none';
            document.getElementById('gameOverScreen').style.display = 'none';
            sword.style.display = 'none';

            // 开始游戏计时
            gameTimer = setInterval(() => {
                timeLeft--;
                document.getElementById('time').textContent = timeLeft;
                
                if (timeLeft <= 0) {
                    gameOver();
                }
            }, 1000);

            // 开始生成水果
            fruitTimer = setInterval(() => {
                if (gameState.gameRunning && !gameState.gameOver) {
                    createFruit();
                }
            }, 800 + Math.random() * 1200); // 每0.8-2秒生成一个水果

            // 立即生成第一个水果
            setTimeout(() => createFruit(), 500);

            startBackgroundMusic();
            gameLoop();
        }

        // 游戏结束
        function gameOver() {
            gameState.gameOver = true;
            gameState.gameRunning = false;
            stopBackgroundMusic();

            if (gameTimer) {
                clearInterval(gameTimer);
                gameTimer = null;
            }
            if (fruitTimer) {
                clearInterval(fruitTimer);
                fruitTimer = null;
            }

            sword.style.display = 'none';

            if (score > highScore) {
                highScore = score;
                localStorage.setItem('fruitNinjaHighScore', highScore);
                document.getElementById('highScore').textContent = highScore;
            }

            document.getElementById('finalScore').textContent = `得分: ${score}`;
            document.getElementById('gameOverScreen').style.display = 'flex';
        }

        // 事件监听
        document.getElementById('startButton').addEventListener('click', () => {
            // 恢复音频上下文(浏览器需要用户交互)
            if (audioContext.state === 'suspended') {
                audioContext.resume();
            }
            initGame();
        });

        document.getElementById('restartButton').addEventListener('click', () => {
            // 恢复音频上下文
            if (audioContext.state === 'suspended') {
                audioContext.resume();
            }
            initGame();
        });
    </script>
</body>
</html>

📖 javascript代码

backgroundMusic.js

// 背景音乐模块
// 使用 Web Audio API 生成背景音乐

let musicInterval = null;
// 注意:audioContext 和 backgroundMusicGain 在主脚本中声明,这里只存储引用
let audioContextRef = null;
let backgroundMusicGainRef = null;

// 初始化背景音乐(需要传入音频上下文和增益节点)
function initBackgroundMusic(audioCtx, musicGain) {
    audioContextRef = audioCtx;
    backgroundMusicGainRef = musicGain;
}

// 获取游戏状态的函数(由主游戏传入)
let getGameState = null;

// 设置游戏状态获取函数
function setGameStateGetter(getter) {
    getGameState = getter;
}

// 生成背景音乐
function startBackgroundMusic() {
    if (!audioContextRef || !backgroundMusicGainRef || musicInterval) return;
    
    try {
        // 激进快速的旋律循环 - 科幻电子风格
        const melody = [
            // 第一段:快速上升
            { freq1: 523.25, freq2: 659.25, duration: 0.2, bass: 261.63 }, // C5, E5, C4低音
            { freq1: 659.25, freq2: 783.99, duration: 0.2, bass: 329.63 }, // E5, G5, E4
            { freq1: 783.99, freq2: 987.77, duration: 0.2, bass: 392.00 }, // G5, B5, G4
            { freq1: 987.77, freq2: 1174.66, duration: 0.2, bass: 493.88 }, // B5, D6, B4
            // 第二段:跳跃音程
            { freq1: 880.00, freq2: 1318.51, duration: 0.2, bass: 440.00 }, // A5, E6, A4
            { freq1: 1046.50, freq2: 1567.98, duration: 0.2, bass: 523.25 }, // C6, G6, C5
            { freq1: 1174.66, freq2: 1760.00, duration: 0.2, bass: 587.33 }, // D6, A6, D5
            // 第三段:快速下降
            { freq1: 1046.50, freq2: 1318.51, duration: 0.2, bass: 523.25 }, // C6, E6, C5
            { freq1: 880.00, freq2: 1108.73, duration: 0.2, bass: 440.00 }, // A5, C#6, A4
            { freq1: 783.99, freq2: 987.77, duration: 0.2, bass: 392.00 }, // G5, B5, G4
            { freq1: 659.25, freq2: 830.61, duration: 0.2, bass: 329.63 }, // E5, G#5, E4
        ];
        
        let noteIndex = 0;
        
        function playNextNote() {
            // 获取当前游戏状态
            if (getGameState) {
                const state = getGameState();
                if (!state.gameRunning || state.gamePaused || state.gameOver) {
                    return;
                }
            }
            
            const note = melody[noteIndex];
            const currentTime = audioContextRef.currentTime;
            
            // 创建主旋律振荡器(高频,更尖锐)
            const osc1 = audioContextRef.createOscillator();
            const osc2 = audioContextRef.createOscillator();
            const gain1 = audioContextRef.createGain();
            const gain2 = audioContextRef.createGain();
            
            osc1.connect(gain1);
            osc2.connect(gain2);
            gain1.connect(backgroundMusicGainRef);
            gain2.connect(backgroundMusicGainRef);
            
            // 使用更激进的波形:方波和锯齿波
            osc1.type = 'square'; // 方波更尖锐
            osc2.type = 'sawtooth'; // 锯齿波更激进
            osc1.frequency.value = note.freq1;
            osc2.frequency.value = note.freq2;
            
            // 更快的起音和衰减,营造冲击感
            gain1.gain.setValueAtTime(0.12, currentTime);
            gain1.gain.exponentialRampToValueAtTime(0.01, currentTime + note.duration);
            gain2.gain.setValueAtTime(0.10, currentTime);
            gain2.gain.exponentialRampToValueAtTime(0.01, currentTime + note.duration);
            
            osc1.start(currentTime);
            osc1.stop(currentTime + note.duration);
            osc2.start(currentTime);
            osc2.stop(currentTime + note.duration);
            
            // 添加低音增强节奏感(如果有低音频率)
            if (note.bass) {
                const bassOsc = audioContextRef.createOscillator();
                const bassGain = audioContextRef.createGain();
                
                bassOsc.connect(bassGain);
                bassGain.connect(backgroundMusicGainRef);
                
                bassOsc.type = 'square'; // 低音使用方波
                bassOsc.frequency.value = note.bass;
                
                bassGain.gain.setValueAtTime(0.08, currentTime);
                bassGain.gain.exponentialRampToValueAtTime(0.01, currentTime + note.duration);
                
                bassOsc.start(currentTime);
                bassOsc.stop(currentTime + note.duration);
            }
            
            noteIndex = (noteIndex + 1) % melody.length;
        }
        
        // 立即播放第一个音符
        playNextNote();
        
        // 设置定时器循环播放 - 更快的节奏(250ms间隔)
        musicInterval = setInterval(() => {
            if (getGameState) {
                const state = getGameState();
                if (state.gameRunning && !state.gamePaused && !state.gameOver) {
                    playNextNote();
                }
            } else {
                playNextNote();
            }
        }, 250); // 每250ms播放一个音符(比原来快一倍)
    } catch (e) {
        console.log('背景音乐生成失败:', e);
    }
}

// 停止背景音乐
function stopBackgroundMusic() {
    if (musicInterval) {
        clearInterval(musicInterval);
        musicInterval = null;
    }
}

 

———— ⬆️·`正文结束`·⬆️————

 


到此这篇文章就介绍到这了,更多精彩内容请关注本人以前的文章或继续浏览下面的文章,创作不易,如果能帮助到大家,希望大家多多支持宝码香车~💕,若转载本文,一定注明本文链接。


整理不易,点赞关注宝码香车

更多专栏订阅推荐:
👍 html+css+js 绚丽效果
💕 vue
✈️ Electron
⭐️ js
📝 字符串
✍️ 时间对象(Date())操作

更多推荐