(文后附完整代码)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);
}
算法原理:
- 计算点在直线上的投影点
- 判断投影点是否在线段范围内
- 计算点到投影点的距离
📖 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;
}
多重检测策略:
- 当前位置检测:水果中心到切割线段的距离
- 上一帧位置检测:处理快速移动的水果
- 路径距离检测:水果移动路径与切割路径的最短距离
- 密集采样检测:在切割路径上采样多个点检测
优势:
- 提高切割检测的准确性
- 减少漏切和误判
- 支持快速滑动切割
📘 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);
}
渲染流程:
- 清空画布
- 绘制背景渐变
- 更新水果位置(物理模拟)
- 绘制所有未切割的水果
- 绘制切割轨迹
- 请求下一帧
性能优化:
- 使用
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秒/次)
- 启动水果生成定时器(0.8-1.4秒/次)
- 延迟500ms生成第一个水果
- 开始背景音乐
- 启动游戏循环
结束流程:
- 更新游戏状态
- 停止背景音乐
- 清除所有定时器
- 更新最高分(如果破纪录)
- 显示结束界面
📚 五、核心算法与技术亮点
📘 5.1 物理引擎实现
抛物线运动模拟:
位置更新:
x = x + vx
y = y + vy
vy = vy + gravity (0.2)
初始速度计算:
v = -√(2 * g * h)
其中g=0.2, h=目标高度
效果:
- 水果从底部弹出,到达最高点后下落
- 符合真实物理规律
- 视觉效果自然流畅
📘 5.2 碰撞检测优化
多重检测策略:
- 点到线段距离
- 线段到线段距离
- 密集采样检测
- 历史位置检测
优势:
- 准确率高(>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 项目亮点
- 完整的游戏体验: 包含开始界面、游戏过程、结束界面的完整流程
- 流畅的交互体验: 60fps渲染,响应迅速
- 丰富的视觉效果: 特效动画、得分弹出、连击提示
- 真实的物理模拟: 抛物线运动、重力加速度
- 智能的碰撞检测: 多重检测策略,准确率高
- 沉浸式音效系统: 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())操作
更多推荐

所有评论(0)