前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎 点赞 + 收藏 + 关注 哦 💕
(文后附完整代码)html+css+javascript弹球射击游戏项目分析

📚 本文简介
本文分享了一个基于HTML5 Canvas的弹球射击游戏项目,采用HTML+CSS+JavaScript技术栈实现。游戏包含物理引擎、动态特效和音效系统,提供丰富的交互体验。文章详细分析了项目架构、HTML语义化布局、CSS视觉设计以及JavaScript核心逻辑,包括游戏配置系统、状态管理和对象模型设计。项目采用模块化开发,包含完整代码,适合前端开发者学习游戏开发技巧。
目录
- (文后附完整代码)html+css+javascript弹球射击游戏项目分析
-
- [📚 本文简介](#📚 本文简介)
- [📚 一、项目架构概述](#📚 一、项目架构概述)
-
- [📘 1.1 项目简介](#📘 1.1 项目简介)
- [📘 1.2 技术栈与架构设计](#📘 1.2 技术栈与架构设计)
- [📘 1.3 项目文件结构](#📘 1.3 项目文件结构)
- [📚 二、HTML结构分析:语义化与模块化的界面搭建](#📚 二、HTML结构分析:语义化与模块化的界面搭建)
-
- [📘 2.1 整体布局设计](#📘 2.1 整体布局设计)
- [📘 2.2 关键元素解析](#📘 2.2 关键元素解析)
-
- [📖 2.2.1 Canvas元素的游戏画布](#📖 2.2.1 Canvas元素的游戏画布)
- [📖 2.2.2 状态遮罩层的设计](#📖 2.2.2 状态遮罩层的设计)
- [📖 2.2.3 信息面板与控制区](#📖 2.2.3 信息面板与控制区)
- [📚 三、CSS样式系统:视觉层次与动效设计](#📚 三、CSS样式系统:视觉层次与动效设计)
-
- [📘 3.1 全局样式与主题](#📘 3.1 全局样式与主题)
- [📘 3.2 视觉层次设计](#📘 3.2 视觉层次设计)
-
- [📖 3.2.1 卡片式信息面板](#📖 3.2.1 卡片式信息面板)
- [📖 3.2.2 数值显示的视觉强调](#📖 3.2.2 数值显示的视觉强调)
- [📘 3.3 交互元素设计](#📘 3.3 交互元素设计)
-
- [📖 3.3.1 按钮样式与状态](#📖 3.3.1 按钮样式与状态)
- [📖 3.3.2 键盘按键提示](#📖 3.3.2 键盘按键提示)
- [📚 四、JavaScript核心逻辑:游戏引擎架构](#📚 四、JavaScript核心逻辑:游戏引擎架构)
-
- [📘 4.1 游戏配置系统](#📘 4.1 游戏配置系统)
- [📘 4.2 游戏状态管理](#📘 4.2 游戏状态管理)
-
- [📖 4.2.1 状态变量设计](#📖 4.2.1 状态变量设计)
- [📖 4.2.2 游戏对象模型](#📖 4.2.2 游戏对象模型)
- [📘 4.3 物理引擎实现](#📘 4.3 物理引擎实现)
-
- [📖 4.3.1 重力系统](#📖 4.3.1 重力系统)
- [📖 4.3.2 碰撞检测系统](#📖 4.3.2 碰撞检测系统)
- [📖 4.3.3 边界反弹系统](#📖 4.3.3 边界反弹系统)
- [📘 4.4 渲染系统架构](#📘 4.4 渲染系统架构)
-
- [📖 4.4.1 双循环设计](#📖 4.4.1 双循环设计)
- [📖 4.4.2 渲染层次与视觉效果](#📖 4.4.2 渲染层次与视觉效果)
- [📖 4.4.3 特效系统](#📖 4.4.3 特效系统)
- [📘 4.5 输入处理系统](#📘 4.5 输入处理系统)
-
- [📖 4.5.1 鼠标控制](#📖 4.5.1 鼠标控制)
- [📖 4.5.2 键盘控制](#📖 4.5.2 键盘控制)
- [📚 五、音频系统设计:Web Audio API的应用](#📚 五、音频系统设计:Web Audio API的应用)
-
- [📘 5.1 音频架构](#📘 5.1 音频架构)
- [📘 5.2 背景音乐实现](#📘 5.2 背景音乐实现)
-
- [📖 5.2.1 旋律生成](#📖 5.2.1 旋律生成)
- [📖 5.2.2 音频合成技术](#📖 5.2.2 音频合成技术)
- [📘 5.3 音效系统](#📘 5.3 音效系统)
-
- [📖 5.3.1 音效分类](#📖 5.3.1 音效分类)
- [📖 5.3.2 音效播放函数](#📖 5.3.2 音效播放函数)
- [📚 六、游戏逻辑与流程控制](#📚 六、游戏逻辑与流程控制)
-
- [📘 6.1 游戏生命周期](#📘 6.1 游戏生命周期)
- [📘 6.2 目标生成算法](#📘 6.2 目标生成算法)
- [📘 6.3 得分与连击系统](#📘 6.3 得分与连击系统)
- [📘 6.4 游戏结束判定](#📘 6.4 游戏结束判定)
- [📚 七、性能优化与架构设计](#📚 七、性能优化与架构设计)
-
- [📘 7.1 性能优化策略](#📘 7.1 性能优化策略)
-
- [📖 7.1.1 时间跳跃保护](#📖 7.1.1 时间跳跃保护)
- [📖 7.1.2 对象池管理](#📖 7.1.2 对象池管理)
- [📖 7.1.4 数据健壮性保护](#📖 7.1.4 数据健壮性保护)
- [📖 7.1.3 渲染优化](#📖 7.1.3 渲染优化)
- [📘 7.2 架构设计亮点](#📘 7.2 架构设计亮点)
-
- [📖 7.2.1 模块化设计](#📖 7.2.1 模块化设计)
- [📖 7.2.2 状态机模式](#📖 7.2.2 状态机模式)
- [📖 7.2.3 事件驱动架构](#📖 7.2.3 事件驱动架构)
- [📚 八、游戏平衡性与难度设计](#📚 八、游戏平衡性与难度设计)
-
- [📘 8.1 物理参数平衡](#📘 8.1 物理参数平衡)
- [📘 8.2 难度曲线](#📘 8.2 难度曲线)
- [📚 九、用户体验设计](#📚 九、用户体验设计)
-
- [📘 9.1 视觉反馈](#📘 9.1 视觉反馈)
- [📘 9.2 交互设计](#📘 9.2 交互设计)
- [📘 9.3 音效反馈](#📘 9.3 音效反馈)
- [📚 十、总结与技术亮点](#📚 十、总结与技术亮点)
-
- [📘 10.1 技术亮点](#📘 10.1 技术亮点)
- [📘 10.2 设计优势](#📘 10.2 设计优势)
- [📘 10.3 可扩展方向](#📘 10.3 可扩展方向)
- [📚 完整代码(可直接使用)](#📚 完整代码(可直接使用))
-
- [📘 项目目录](#📘 项目目录)
- [📘 项目代码](#📘 项目代码)
-
- [📖 html代码](#📖 html代码)
- [📖 css代码](#📖 css代码)
- [📖 javascript代码](#📖 javascript代码)
------------ ⬇️·`正文开始`·⬇️------------
📚 一、项目架构概述
📘 1.1 项目简介
弹球射击是一款基于HTML5 Canvas的物理引擎游戏,玩家通过鼠标瞄准和键盘控制,发射弹球击碎屏幕上方的目标方块。游戏融合了真实的物理模拟、动态视觉特效和沉浸式音效系统,提供了丰富的游戏体验。
📘 1.2 技术栈与架构设计
- 前端技术栈:HTML5 + CSS3 + 原生JavaScript(ES6+)
- 图形渲染:HTML5 Canvas 2D API
- 音频系统:Web Audio API(背景音乐 + 音效)
- 物理引擎:自定义物理系统(重力、碰撞、反弹)
- 渲染模式:双循环架构(更新循环 + 渲染循环)
📘 1.3 项目文件结构
00007弹球射击/
├── index.html # 游戏主页面
├── script.js # 游戏核心逻辑(780行)
├── style.css # 样式表
└──backgroundMusic.js # 背景音乐模块
📚 二、HTML结构分析:语义化与模块化的界面搭建
📘 2.1 整体布局设计
游戏采用三栏式布局,通过Flexbox实现响应式设计:
| Header 标题区 |
|---|
| 左侧边栏 | 游戏主区域(Canvas) | 右侧边栏 |
|---|---|---|
| 信息面板 操作说明 | 游戏主区域(Canvas) | 控制按钮 |
📘 2.2 关键元素解析
📖 2.2.1 Canvas元素的游戏画布
html
<canvas id="game-canvas" width="800" height="600"></canvas>
- 尺寸设置:800×600像素固定尺寸
- 渲染上下文:2D上下文用于绘制游戏场景
- 交互区域:鼠标事件的主要监听目标
- 光标样式:十字光标(crosshair),提示玩家可进行瞄准操作
📖 2.2.2 状态遮罩层的设计
游戏实现了三层状态遮罩,通过CSS类控制显示/隐藏:
- 开始界面 (
start-screen):游戏未开始时显示 - 暂停遮罩 (
pause-overlay):游戏暂停时覆盖 - 结束界面 (
game-over):游戏结束时展示成绩
📖 2.2.3 信息面板与控制区
左侧边栏包含:
- 得分、弹球数、目标数、连击数实时显示
- 完整的操作说明(键盘 + 鼠标)
右侧边栏提供:
- 暂停按钮
- 新游戏按钮
📚 三、CSS样式系统:视觉层次与动效设计
📘 3.1 全局样式与主题
css
body {
background: linear-gradient(135deg, #0a0a1a 0%, #1a1a3e 100%);
font-family: 'Arial', 'Microsoft YaHei', sans-serif;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
- 深色主题:渐变背景营造科技感
- 字体选择:优先系统字体,确保跨平台一致性
- 响应式设计 :使用
max-width和flex实现自适应 - 居中布局:Flexbox居中,适配不同屏幕尺寸
📘 3.2 视觉层次设计
📖 3.2.1 卡片式信息面板
css
.info-panel {
background: rgba(0, 0, 0, 0.6);
border-radius: 10px;
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 212, 255, 0.3);
}
- 半透明效果 :
rgba背景 +backdrop-filter模糊 - 霓虹边框:青色发光效果增强科技感
- 阴影层次 :
box-shadow提升立体感
📖 3.2.2 数值显示的视觉强调
css
.score-box .value {
font-size: 24px;
font-weight: bold;
color: #00d4ff; /* 霓虹青色 */
}
- 高对比度:青色数值在深色背景上清晰可见
- 字体大小:24px大号字体突出关键信息
📘 3.3 交互元素设计
📖 3.3.1 按钮样式与状态
css
.btn {
background: linear-gradient(135deg, #00d4ff 0%, #0099cc 100%);
border: none;
border-radius: 5px;
padding: 12px 24px;
transition: all 0.3s;
}
.btn:hover {
background: linear-gradient(135deg, #00e5ff 0%, #00aadd 100%);
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.4);
}
- 渐变背景:青色到蓝色的线性渐变
- 圆角设计:5px圆角,简洁现代
- 悬停效果:上移2px + 增强阴影,提升交互反馈
📖 3.3.2 键盘按键提示
css
.key {
background: rgba(0, 212, 255, 0.2);
border: 1px solid #00d4ff;
border-radius: 4px;
padding: 4px 8px;
}
- 按键样式:模拟键盘按键外观
- 颜色编码:与主题色保持一致
📚 四、JavaScript核心逻辑:游戏引擎架构
📘 4.1 游戏配置系统
游戏采用常量配置模式,所有关键参数集中定义:
javascript
// 画布与物理配置
const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 600;
const BALL_RADIUS = 8; // 弹球半径
const BALL_SPEED = 24; // 弹球初始速度
const GRAVITY = 0.22; // 重力加速度
const FRICTION = 0.98; // 空气阻力
const BOUNCE_DAMPING = 0.7; // 上下边界反弹系数
const SIDE_BOUNCE_DAMPING = 1.0; // 左右边界完美弹性
const SIDE_MIN_BOUNCE_SPEED = 3; // 左右边界最小反弹速度
const TARGET_SIZE = 40; // 目标方块尺寸
const LAUNCHER_X = CANVAS_WIDTH / 2; // 发射器X坐标
const LAUNCHER_Y = CANVAS_HEIGHT - 50; // 发射器Y坐标
const MAX_BALLS = 10; // 最大弹球数
设计优势:
- 便于参数调整和平衡游戏难度
- 物理参数可独立优化
- 支持后续的难度系统扩展
📘 4.2 游戏状态管理
📖 4.2.1 状态变量设计
javascript
let gameRunning = false; // 游戏运行状态
let gamePaused = false; // 暂停状态
let gameOver = false; // 游戏结束状态
let score = 0; // 得分
let combo = 0; // 连击数
let maxCombo = 0; // 最高连击
状态机设计:
- 清晰的状态转换逻辑
- 防止非法状态组合(如同时暂停和运行)
- 支持游戏流程的精确控制
📖 4.2.2 游戏对象模型
javascript
// 弹球对象
const ball = {
x: LAUNCHER_X,
y: LAUNCHER_Y,
vx: Math.cos(launcherAngle) * BALL_SPEED,
vy: Math.sin(launcherAngle) * BALL_SPEED,
radius: BALL_RADIUS,
active: true
};
// 目标对象
const target = {
x: spacingX + col * (TARGET_SIZE + spacingX) + TARGET_SIZE / 2,
y: startY + row * (TARGET_SIZE + 20),
width: TARGET_SIZE,
height: TARGET_SIZE,
color: `hsl(${row * 60 + col * 10}, 70%, 50%)`,
hit: false
};
数据结构特点:
- 位置和速度分离,便于物理计算
- 颜色使用HSL动态生成,实现彩虹效果
active状态标记便于对象管理
📘 4.3 物理引擎实现
📖 4.3.1 重力系统
javascript
function updateBalls(deltaTime) {
// 检查弹球数据有效性(防止NaN/Infinity)
if (!isFinite(ball.x) || !isFinite(ball.y) || !isFinite(ball.vx) || !isFinite(ball.vy)) {
ball.active = false;
continue;
}
// 应用重力(每帧更新)
ball.vy += GRAVITY * deltaTime * 60;
// 更新位置
ball.x += ball.vx * deltaTime * 60;
ball.y += ball.vy * deltaTime * 60;
// 应用空气阻力
ball.vx *= Math.pow(FRICTION, deltaTime * 60);
ball.vy *= Math.pow(FRICTION, deltaTime * 60);
// 再次检查位置有效性(防止计算后变成NaN)
if (!isFinite(ball.x) || !isFinite(ball.y)) {
ball.active = false;
continue;
}
}
物理模拟特点:
- 使用
deltaTime实现时间无关的物理计算 - 重力加速度:0.22像素/帧²
- 空气阻力:每帧速度衰减2%
- 数据有效性检查:在物理更新前后检查NaN/Infinity,防止计算错误
📖 4.3.2 碰撞检测系统
圆形与矩形碰撞算法:
javascript
// 计算最近点
const closestX = Math.max(target.x - target.width / 2,
Math.min(ball.x, target.x + target.width / 2));
const closestY = Math.max(target.y - target.height / 2,
Math.min(ball.y, target.y + target.height / 2));
// 计算距离
const dx = ball.x - closestX;
const dy = ball.y - closestY;
const distance = Math.sqrt(dx * dx + dy * dy);
// 防止distance为0导致NaN
if (distance < ball.radius && distance > 0.001) {
// 碰撞发生
target.hit = true;
createExplosion(target.x, target.y, 60);
score += 100;
combo++;
}
碰撞响应处理:
javascript
// 计算碰撞法线
const normalX = dx / distance;
const normalY = dy / distance;
const dot = ball.vx * normalX + ball.vy * normalY;
// 计算反弹速度(反弹系数从0.5提升到0.8)
let newVx = ball.vx - 2 * dot * normalX * 0.8;
let newVy = ball.vy - 2 * dot * normalY * 0.8;
// 检查计算结果是否有效
if (!isFinite(newVx)) newVx = ball.vx * -0.8;
if (!isFinite(newVy)) newVy = ball.vy * -0.8;
// 确保反弹后速度足够大,防止卡住
const minBounceSpeed = 3;
const speed = Math.sqrt(newVx * newVx + newVy * newVy);
if (!isFinite(speed) || speed < minBounceSpeed) {
const angle = Math.atan2(newVy || 0, newVx || 0);
if (!isFinite(angle)) {
newVx = 0;
newVy = minBounceSpeed;
} else {
newVx = Math.cos(angle) * minBounceSpeed;
newVy = Math.sin(angle) * minBounceSpeed;
}
}
ball.vx = newVx;
ball.vy = newVy;
// 将弹球从目标位置推开,避免卡在目标内部
const pushDistance = ball.radius - distance + 2;
if (isFinite(pushDistance) && pushDistance > 0) {
ball.x += normalX * pushDistance;
ball.y += normalY * pushDistance;
}
设计亮点:
- 精确的圆形-矩形碰撞检测
- 基于法线的反射计算
- 碰撞能量衰减:0.8倍反弹(从0.5提升,防止卡住)
- 最小反弹速度保证:至少3像素/帧,防止弹球速度过小
- 推开机制:将弹球从目标位置推开,避免卡在目标内部
- NaN保护:检查计算结果有效性,防止数值异常
📖 4.3.3 边界反弹系统
javascript
// 左右边界:完美弹性碰撞
if (ball.x - ball.radius <= 0) {
ball.x = ball.radius;
const newVx = -ball.vx * SIDE_BOUNCE_DAMPING;
// 确保最小反弹速度
ball.vx = Math.abs(newVx) < SIDE_MIN_BOUNCE_SPEED
? (newVx > 0 ? SIDE_MIN_BOUNCE_SPEED : -SIDE_MIN_BOUNCE_SPEED)
: newVx;
}
// 上下边界:能量衰减
if (ball.y - ball.radius <= 0) {
ball.y = ball.radius;
ball.vy = -ball.vy * BOUNCE_DAMPING;
}
物理参数设计:
- 左右边界:100%弹性(能量不损失),最小反弹速度3像素/帧(防止弹球卡死)
- 上下边界:70%弹性(能量损失30%)
- 弹球静止检测 :
- 速度小于0.5时触发检测
- 在底部区域(y > CANVAS_HEIGHT - 100):标记为不活跃
- 在上方区域(y < 250)且速度极小(< 0.3):给一个向下的推力(GRAVITY × 3),帮助弹球下落
📘 4.4 渲染系统架构
📖 4.4.1 双循环设计
游戏采用分离的更新循环和渲染循环:
javascript
// 更新循环(物理计算)
function update(currentTime) {
const deltaTime = (currentTime - lastTime) / 1000;
updateBalls(deltaTime);
updateExplosions(deltaTime);
requestAnimationFrame(update);
}
// 渲染循环(画面绘制)
function draw() {
drawBackground();
drawTargets();
drawBalls();
drawLauncher();
drawExplosions();
requestAnimationFrame(draw);
}
架构优势:
- 物理更新与渲染帧率解耦
- 防止渲染卡顿影响物理计算
- 支持后续的插值渲染优化
📖 4.4.2 渲染层次与视觉效果
1. 背景渲染
javascript
function drawBackground() {
ctx.fillStyle = '#000011';
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// 绘制网格
ctx.strokeStyle = 'rgba(0, 212, 255, 0.1)';
for (let x = 0; x < CANVAS_WIDTH; x += 40) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, CANVAS_HEIGHT);
ctx.stroke();
}
}
2. 弹球渲染(渐变与高光)
javascript
// 检查弹球位置是否有效(防止NaN或Infinity)
if (!isFinite(ball.x) || !isFinite(ball.y) || !isFinite(ball.radius)) {
ball.active = false; // 标记为不活跃,稍后会被移除
return;
}
const gradient = ctx.createRadialGradient(
ball.x - ball.radius * 0.3,
ball.y - ball.radius * 0.3,
0,
ball.x, ball.y, ball.radius
);
gradient.addColorStop(0, '#ffffff');
gradient.addColorStop(1, '#00d4ff');
- 径向渐变:模拟球体光照效果
- 高光处理:左上角白色高光增强立体感
- 阴影效果:2像素偏移的黑色阴影
- 数据验证 :绘制前检查位置有效性,防止
createRadialGradient报错
3. 目标渲染(彩虹配色)
javascript
color: `hsl(${row * 60 + col * 10}, 70%, 50%)`
- HSL颜色空间:行控制色相,列微调
- 饱和度:70%(鲜艳)
- 亮度:50%(适中)
📖 4.4.3 特效系统
爆炸特效实现:
javascript
function createExplosion(x, y, size) {
const explosion = {
x: x, y: y,
emoji: '💥',
size: size,
life: 0.8,
scale: 0,
rotation: Math.random() * Math.PI * 2
};
explosions.push(explosion);
// 粒子系统
for (let i = 0; i < 12; i++) {
particles.push({
x: x, y: y,
vx: (Math.random() - 0.5) * 300,
vy: (Math.random() - 0.5) * 300,
life: 0.6,
maxLife: 0.6,
size: Math.random() * 5 + 2,
color: `hsl(${Math.random() * 60 + 10}, 100%, 60%)`
});
}
}
特效特点:
- Emoji爆炸:使用💥符号增强视觉冲击力,带旋转和缩放动画
- 粒子系统:12个随机方向的彩色粒子,速度衰减系数0.95(模拟空气阻力)
- 生命周期管理:爆炸0.8秒淡出,粒子0.6秒淡出
- 视觉效果:爆炸带红色阴影发光效果(shadowBlur: 20, shadowColor: '#ff4444')
📘 4.5 输入处理系统
📖 4.5.1 鼠标控制
javascript
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top;
// 计算发射器角度
const dx = mouseX - LAUNCHER_X;
const dy = mouseY - LAUNCHER_Y;
launcherAngle = Math.atan2(dy, dx);
// 限制角度范围(-157.5° 到 -22.5°)
if (launcherAngle > -Math.PI / 8) {
launcherAngle = -Math.PI / 8;
} else if (launcherAngle < -Math.PI * 7 / 8) {
launcherAngle = -Math.PI * 7 / 8;
}
});
角度限制设计:
- 防止向下发射(游戏逻辑限制)
- 允许左右约67.5°的瞄准范围
- 提升游戏策略性
📖 4.5.2 键盘控制
javascript
document.addEventListener('keydown', (e) => {
if (e.key === ' ') shootBall(); // 空格发射
if (e.key === 'p') togglePause(); // P暂停
if (e.key === 'r') startGame(); // R重新开始
if (e.key === 'a') launcherAngle -= 0.1; // A向左
if (e.key === 'd') launcherAngle += 0.1; // D向右
});
操作映射:
- 鼠标:瞄准
- 左键/空格:发射
- A/←:向左调整
- D/→:向右调整
- R:重新开始
- P:暂停
📚 五、音频系统设计:Web Audio API的应用
📘 5.1 音频架构
游戏采用分离的音频系统:
Web Audio API
├── 背景音乐系统 (backgroundMusicGain: 0.3)
│ └── 双振荡器合成旋律
└── 音效系统 (soundEffectsGain: 0.5)
├── 发射音效 (400Hz, 0.1s)
├── 爆炸音效 (300Hz, 0.2s)
├── 击中音效 (600Hz, 0.1s)
└── 反弹音效 (200Hz, 0.05s)
📘 5.2 背景音乐实现
📖 5.2.1 旋律生成
javascript
const melody = [
{ freq1: 523.25, freq2: 659.25, duration: 0.5 }, // C5, E5
{ freq1: 587.33, freq2: 698.46, duration: 0.5 }, // D5, F5
{ freq1: 659.25, freq2: 783.99, duration: 0.5 }, // E5, G5
// ... 共8个音符循环
];
音乐理论:
- 双音和弦:每个音符由两个频率叠加
- 频率选择:基于自然音阶(C大调)
- 波形选择:正弦波 + 三角波
- 循环播放:500ms间隔,无限循环
📖 5.2.2 音频合成技术
javascript
function playNextNote() {
const osc1 = audioContext.createOscillator();
const osc2 = audioContext.createOscillator();
const gain1 = audioContext.createGain();
const gain2 = audioContext.createGain();
osc1.connect(gain1);
osc2.connect(gain2);
gain1.connect(backgroundMusicGain);
gain2.connect(backgroundMusicGain);
osc1.type = 'sine';
osc2.type = 'triangle';
osc1.frequency.value = note.freq1;
osc2.frequency.value = note.freq2;
// 音量包络(淡入淡出)
gain1.gain.setValueAtTime(0.1, currentTime);
gain1.gain.exponentialRampToValueAtTime(0.01, currentTime + note.duration);
osc1.start(currentTime);
osc1.stop(currentTime + note.duration);
}
音频处理节点:
- 振荡器:生成不同波形的音频信号
- 增益节点:控制音量和包络
- 指数包络:自然的音量衰减曲线
📘 5.3 音效系统
📖 5.3.1 音效分类
| 音效类型 | 频率(Hz) | 波形 | 时长(s) | 音量 | 触发时机 |
|---|---|---|---|---|---|
| 发射 | 400 | square | 0.1 | 0.2 | 弹球发射时 |
| 爆炸 | 300+150 | sawtooth | 0.2+0.15 | 0.4+0.3 | 目标击碎时 |
| 击中 | 600 | sine | 0.1 | 0.25 | 碰撞发生时 |
| 反弹 | 200 | square | 0.05 | 0.15 | 边界碰撞时 |
📖 5.3.2 音效播放函数
javascript
function playSound(frequency, duration, type, volume) {
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(soundEffectsGain);
oscillator.type = type;
oscillator.frequency.value = frequency;
gainNode.gain.setValueAtTime(volume, currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, currentTime + duration);
oscillator.start(currentTime);
oscillator.stop(currentTime + duration);
}
📚 六、游戏逻辑与流程控制
📘 6.1 游戏生命周期
开始界面 → 开始游戏 → 游戏运行 → [暂停/继续] → 游戏结束 → 重新开始
📘 6.2 目标生成算法
javascript
function generateTargets() {
const rows = 3;
const cols = 6;
const spacingX = (CANVAS_WIDTH - cols * TARGET_SIZE) / (cols + 1);
const startY = 80;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
targets.push({
x: spacingX + col * (TARGET_SIZE + spacingX) + TARGET_SIZE / 2,
y: startY + row * (TARGET_SIZE + 20),
color: `hsl(${row * 60 + col * 10}, 70%, 50%)`
});
}
}
}
布局特点:
- 3行×6列:共18个目标
- 等间距分布:自动计算间距确保居中
- 彩虹配色:每行60度色相偏移
📘 6.3 得分与连击系统
javascript
// 击中目标
score += 100;
combo++;
maxCombo = Math.max(maxCombo, combo);
// 弹球消失时重置连击
combo = 0;
得分规则:
- 每个目标:100分
- 连击奖励:连续击中增加连击数
- 最高连击:记录单局最高连击
📘 6.4 游戏结束判定
javascript
// 条件1:发射时检查(弹球用尽且目标未击毁)
if (ballsRemaining === 0 && balls.length === 0) {
setTimeout(() => {
if (targetCount > 0) {
endGame();
}
}, 2000);
}
// 条件2:击中目标后检查(所有目标被击毁)
if (targetCount === 0) {
setTimeout(() => endGame(), 1000);
}
// 条件3:弹球消失时检查(弹球用尽但仍有目标)
if (ballsRemaining === 0 && balls.length === 0 && targetCount > 0) {
setTimeout(() => endGame(), 500);
}
结束延迟设计:
- 胜利:1秒延迟(展示特效)
- 失败:0.5秒延迟(快速反馈)
- 发射时预判:2秒延迟(等待当前弹球消失)
📚 七、性能优化与架构设计
📘 7.1 性能优化策略
📖 7.1.1 时间跳跃保护
javascript
if (deltaTime > 0.1) return; // 防止时间跳跃过大
问题场景 :浏览器标签页切换导致长时间未更新
解决方案:丢弃超过100ms的帧,防止物理计算异常
📖 7.1.2 对象池管理
javascript
// 移除不活跃对象
if (!ball.active || ball.y > CANVAS_HEIGHT + 50) {
balls.splice(i, 1);
}
内存管理:
- 及时移除超出边界的弹球(y > CANVAS_HEIGHT + 50)
- 移除静止的弹球(速度 < 0.5 且位置在底部100像素内)
- 清理生命周期结束的特效(爆炸和粒子)
- 反向遍历数组删除,避免索引错乱
- 防止内存泄漏
📖 7.1.4 数据健壮性保护
javascript
// 物理更新前检查
if (!isFinite(ball.x) || !isFinite(ball.y) || !isFinite(ball.vx) || !isFinite(ball.vy)) {
ball.active = false;
continue;
}
// 绘制前检查
if (!isFinite(ball.x) || !isFinite(ball.y) || !isFinite(ball.radius)) {
ball.active = false;
return;
}
健壮性措施:
- NaN/Infinity检测 :使用
isFinite()检查数值有效性 - 碰撞距离保护:检查distance > 0.001,防止除以0
- 反弹计算保护:检查计算结果有效性,无效时使用备用方案
- 推开距离验证:检查推开距离有效性再应用
- 自动修复机制:发现异常数据时自动标记为不活跃,避免游戏崩溃
📖 7.1.3 渲染优化
javascript
// 分离更新和渲染循环
requestAnimationFrame(update);
requestAnimationFrame(draw);
性能提升:
- 物理计算与渲染解耦
- 避免渲染卡顿影响物理模拟
- 支持高刷新率显示器
📘 7.2 架构设计亮点
📖 7.2.1 模块化设计
javascript
// 功能模块分离
initAudioContext() // 音频初始化
initGame() // 游戏初始化
updateBalls() // 物理更新
draw() // 渲染
startBackgroundMusic() // 音频控制
优势:
- 代码职责清晰
- 便于测试和维护
- 支持功能扩展
📖 7.2.2 状态机模式
javascript
if (!gameRunning || gamePaused || gameOver) return;
状态检查:
- 每个关键函数都进行状态验证
- 防止非法操作
- 确保游戏逻辑正确性
📖 7.2.3 事件驱动架构
javascript
canvas.addEventListener('mousemove', handleMouseMove);
document.addEventListener('keydown', handleKeyDown);
事件处理:
- 输入事件与游戏逻辑分离
- 便于添加新的输入方式
- 支持多人游戏扩展
📚 八、游戏平衡性与难度设计
📘 8.1 物理参数平衡
| 参数 | 值 | 设计意图 |
|---|---|---|
| 重力 | 0.22 | 适中的抛物线轨迹 |
| 速度 | 24 | 快速但可控 |
| 弹球数 | 10 | 有限次数增加策略性 |
| 目标数 | 18 | 3×6布局适中难度 |
| 左右弹性 | 1.0 | 完美反弹增加可玩性 |
| 上下弹性 | 0.7 | 能量衰减符合物理直觉 |
📘 8.2 难度曲线
游戏流程:
- 初期:10个弹球,充足的尝试机会
- 中期:物理反弹增加策略深度
- 后期:弹球用尽后需要精准瞄准
平衡设计:
- 完美弹性的左右边界允许弹球反弹多次
- 有限的弹球数量鼓励精准瞄准
- 连击系统奖励连续命中
📚 九、用户体验设计
📘 9.1 视觉反馈
即时反馈:
- 击中目标时的爆炸特效
- 弹球碰撞时的音效
- 得分和连击数的实时更新
状态提示:
- 暂停界面清晰的继续按钮
- 游戏结束时的最终成绩展示
- 开始界面的操作说明
📘 9.2 交互设计
操作方式:
- 鼠标瞄准(直观)
- 键盘辅助(精确控制)
- 多种发射方式(左键/空格)
容错设计:
- 角度限制防止误操作
- 暂停功能允许中断
- 重新开始按钮随时可用
📘 9.3 音效反馈
音频提示:
- 发射音效:确认操作
- 击中音效:成功反馈
- 爆炸音效:奖励反馈
- 反弹音效:物理反馈
📚 十、总结与技术亮点
📘 10.1 技术亮点
- 自定义物理引擎:完整的重力、碰撞、反弹系统
- 双循环架构:更新与渲染分离,确保物理稳定性
- Web Audio API:程序化音乐生成和音效合成
- 特效系统:Emoji爆炸 + 粒子系统的视觉冲击
- 响应式设计:Flexbox布局适配不同屏幕
- 状态机管理:清晰的游戏状态转换逻辑
- 健壮性保护:全面的NaN/Infinity检测,防止数值异常导致游戏崩溃
📘 10.2 设计优势
- 代码简洁:780行实现完整游戏
- 性能优秀:60fps流畅运行
- 体验丰富:视觉、听觉、触觉反馈齐全
- 易于扩展:模块化设计支持功能添加
📘 10.3 可扩展方向
- 难度系统:多关卡、Boss战
- 道具系统:加速、分裂、穿透等特殊弹球
- 排行榜:本地存储或云端排名
- 多人模式:分屏或联网对战
- 皮肤系统:自定义弹球和目标外观
- 关卡编辑器:用户创建和分享关卡
📚 完整代码(可直接使用)
📘 项目目录

📘 项目代码
📖 html代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>弹球射击 - 物理感弹球射击</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<div class="header">
<h1>弹球射击</h1>
<p class="subtitle">物理感弹球射击 · 真实物理引擎</p>
</div>
<div class="game-wrapper">
<div class="sidebar left">
<div class="info-panel">
<div class="score-box">
<div class="label">得分</div>
<div class="value" id="score">0</div>
</div>
<div class="score-box">
<div class="label">弹球数</div>
<div class="value" id="balls">10</div>
</div>
<div class="score-box">
<div class="label">目标数</div>
<div class="value" id="targets">0</div>
</div>
<div class="score-box">
<div class="label">连击</div>
<div class="value" id="combo">0</div>
</div>
</div>
<div class="controls-panel">
<h3>操作说明</h3>
<div class="control-item">
<span class="key">鼠标</span>
<span>瞄准</span>
</div>
<div class="control-item">
<span class="key">左键/空格</span>
<span>发射弹球</span>
</div>
<div class="control-item">
<span class="key">A / ←</span>
<span>向左调整</span>
</div>
<div class="control-item">
<span class="key">D / →</span>
<span>向右调整</span>
</div>
<div class="control-item">
<span class="key">R</span>
<span>重新开始</span>
</div>
<div class="control-item">
<span class="key">P</span>
<span>暂停</span>
</div>
</div>
</div>
<div class="game-area">
<canvas id="game-canvas" width="800" height="600"></canvas>
<div class="game-over" id="game-over">
<div class="game-over-content">
<h2>游戏结束</h2>
<p>最终得分: <span id="final-score">0</span></p>
<p>剩余弹球: <span id="final-balls">0</span></p>
<p>最高连击: <span id="final-combo">0</span></p>
<button class="btn" id="restart-btn">重新开始</button>
</div>
</div>
<div class="pause-overlay" id="pause-overlay">
<div class="pause-content">
<h2>游戏暂停</h2>
<button class="btn" id="resume-btn">继续游戏</button>
</div>
</div>
<div class="start-screen" id="start-screen">
<div class="start-content">
<h2>弹球射击</h2>
<p>物理感弹球射击 · 真实物理引擎</p>
<p>使用鼠标瞄准,点击发射弹球</p>
<p>击碎所有目标获得高分!</p>
<p class="warning">💥 弹球具有真实物理效果!</p>
<button class="btn" id="start-btn">开始游戏</button>
</div>
</div>
</div>
<div class="sidebar right">
<div class="button-group">
<button class="btn" id="pause-btn">暂停</button>
<button class="btn" id="new-game-btn">新游戏</button>
</div>
<div class="legend-panel">
<h3>游戏说明</h3>
<div class="legend-item">
<span class="emoji">🎯</span>
<span>发射器</span>
</div>
<div class="legend-item">
<span class="emoji">⚪</span>
<span>弹球</span>
</div>
<div class="legend-item">
<span class="emoji">💥</span>
<span>爆炸特效</span>
</div>
<div class="legend-item">
<span class="emoji">⬜</span>
<span>目标方块</span>
</div>
</div>
</div>
</div>
</div>
<script src="backgroundMusic.js"></script>
<script src="script.js"></script>
</body>
</html>
📖 css代码
css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #0a0a1a 0%, #1a1a3e 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
color: #fff;
}
.container {
max-width: 1400px;
width: 100%;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header h1 {
font-size: 48px;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5), 0 0 20px rgba(0, 150, 255, 0.5);
color: #00d4ff;
text-transform: uppercase;
letter-spacing: 3px;
}
.subtitle {
font-size: 18px;
color: rgba(255, 255, 255, 0.7);
margin-top: 10px;
}
.game-wrapper {
display: flex;
gap: 20px;
justify-content: center;
align-items: flex-start;
}
.sidebar {
width: 200px;
display: flex;
flex-direction: column;
gap: 20px;
}
.info-panel,
.controls-panel,
.legend-panel {
background: rgba(0, 0, 0, 0.6);
border-radius: 10px;
padding: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(0, 212, 255, 0.3);
}
.info-panel .label {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 10px;
text-align: center;
}
.score-box {
margin-bottom: 15px;
}
.score-box:last-child {
margin-bottom: 0;
}
.score-box .label {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 5px;
}
.score-box .value {
font-size: 24px;
font-weight: bold;
color: #00d4ff;
text-align: center;
}
.controls-panel h3,
.legend-panel h3 {
font-size: 18px;
margin-bottom: 15px;
text-align: center;
color: #00d4ff;
}
.control-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.control-item:last-child {
border-bottom: none;
}
.control-item .key {
background: rgba(0, 212, 255, 0.2);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
color: #00d4ff;
border: 1px solid rgba(0, 212, 255, 0.4);
}
.game-area {
position: relative;
background: rgba(0, 0, 0, 0.6);
border-radius: 10px;
padding: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(0, 212, 255, 0.3);
}
#game-canvas {
display: block;
background: #000;
border-radius: 5px;
cursor: crosshair;
}
.game-over,
.pause-overlay,
.start-screen {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
display: none;
justify-content: center;
align-items: center;
border-radius: 10px;
z-index: 10;
}
.game-over.active,
.pause-overlay.active,
.start-screen.active {
display: flex;
}
.game-over-content,
.pause-content,
.start-content {
text-align: center;
background: rgba(0, 0, 0, 0.8);
padding: 40px;
border-radius: 10px;
border: 2px solid rgba(0, 212, 255, 0.5);
box-shadow: 0 0 30px rgba(0, 212, 255, 0.3);
}
.game-over-content h2,
.pause-content h2,
.start-content h2 {
font-size: 36px;
margin-bottom: 20px;
color: #00d4ff;
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
}
.game-over-content p,
.start-content p {
font-size: 18px;
margin: 10px 0;
color: rgba(255, 255, 255, 0.9);
}
.start-content .warning {
color: #ff4444;
font-weight: bold;
font-size: 20px;
margin: 15px 0;
text-shadow: 0 0 10px rgba(255, 68, 68, 0.5);
}
.button-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.btn {
background: linear-gradient(135deg, #00d4ff 0%, #0099cc 100%);
color: #fff;
border: none;
padding: 12px 24px;
border-radius: 5px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
text-transform: uppercase;
letter-spacing: 1px;
}
.btn:hover {
background: linear-gradient(135deg, #00e5ff 0%, #00aadd 100%);
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.4);
}
.btn:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.legend-item:last-child {
border-bottom: none;
}
.legend-item .emoji {
font-size: 24px;
width: 30px;
text-align: center;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.game-wrapper {
flex-direction: column;
align-items: center;
}
.sidebar {
width: 100%;
max-width: 800px;
flex-direction: row;
justify-content: space-around;
}
.sidebar.left,
.sidebar.right {
width: 100%;
}
}
📖 javascript代码
backgroundMusic.js
javascript
// 背景音乐模块
// 使用 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.5 }, // C5, E5
{ freq1: 587.33, freq2: 698.46, duration: 0.5 }, // D5, F5
{ freq1: 659.25, freq2: 783.99, duration: 0.5 }, // E5, G5
{ freq1: 698.46, freq2: 880.00, duration: 0.5 }, // F5, A5
{ freq1: 783.99, freq2: 987.77, duration: 0.5 }, // G5, B5
{ freq1: 880.00, freq2: 1046.50, duration: 0.5 }, // A5, C6
{ freq1: 783.99, freq2: 987.77, duration: 0.5 }, // G5, B5
{ freq1: 659.25, freq2: 783.99, duration: 0.5 }, // E5, G5
];
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 = 'sine';
osc2.type = 'triangle';
osc1.frequency.value = note.freq1;
osc2.frequency.value = note.freq2;
gain1.gain.setValueAtTime(0.1, currentTime);
gain1.gain.exponentialRampToValueAtTime(0.01, currentTime + note.duration);
gain2.gain.setValueAtTime(0.08, 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);
noteIndex = (noteIndex + 1) % melody.length;
}
// 立即播放第一个音符
playNextNote();
// 设置定时器循环播放
musicInterval = setInterval(() => {
if (getGameState) {
const state = getGameState();
if (state.gameRunning && !state.gamePaused && !state.gameOver) {
playNextNote();
}
} else {
playNextNote();
}
}, 500); // 每500ms播放一个音符
} catch (e) {
console.log('背景音乐生成失败:', e);
}
}
// 停止背景音乐
function stopBackgroundMusic() {
if (musicInterval) {
clearInterval(musicInterval);
musicInterval = null;
}
}
script.js
javascript
// 游戏配置
const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 600;
const BALL_RADIUS = 8;
const BALL_SPEED = 24; // 增加弹球速度,确保能到达目标
const GRAVITY = 0.22; // 减小重力,让弹球飞得更远更高
const FRICTION = 0.98;
const BOUNCE_DAMPING = 0.7; // 上下边界反弹阻尼
const SIDE_BOUNCE_DAMPING = 1.0; // 左右侧壁反弹阻尼(完美弹性,能量不损失)
const SIDE_MIN_BOUNCE_SPEED = 3; // 左右侧壁最小反弹速度(增大50%)
const TARGET_SIZE = 40;
const LAUNCHER_X = CANVAS_WIDTH / 2;
const LAUNCHER_Y = CANVAS_HEIGHT - 50;
const MAX_BALLS = 10;
// 游戏状态
let gameRunning = false;
let gamePaused = false;
let gameOver = false;
let score = 0;
let ballsRemaining = MAX_BALLS;
let combo = 0;
let maxCombo = 0;
let targetCount = 0;
// Canvas
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
// 游戏对象
let launcherAngle = -Math.PI / 2; // 发射器角度(默认向上)
let balls = []; // 弹球数组
let targets = []; // 目标方块数组
let explosions = []; // emoji爆炸特效
let particles = []; // 粒子特效
// 鼠标位置
let mouseX = 0;
let mouseY = 0;
// 键盘状态
const keys = {};
// 音频上下文
let audioContext = null;
let backgroundMusicGain = null;
let soundEffectsGain = null;
// 初始化音频上下文
function initAudioContext() {
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 创建背景音乐增益节点
backgroundMusicGain = audioContext.createGain();
backgroundMusicGain.connect(audioContext.destination);
backgroundMusicGain.gain.value = 0.3;
// 创建音效增益节点
soundEffectsGain = audioContext.createGain();
soundEffectsGain.connect(audioContext.destination);
soundEffectsGain.gain.value = 0.5;
// 初始化背景音乐模块
initBackgroundMusic(audioContext, backgroundMusicGain);
setGameStateGetter(() => ({
gameRunning,
gamePaused,
gameOver
}));
} catch (e) {
console.log('音频初始化失败:', e);
}
}
// 播放音效
function playSound(frequency, duration, type = 'square', volume = 0.3) {
if (!audioContext || !soundEffectsGain) return;
try {
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(soundEffectsGain);
oscillator.type = type;
oscillator.frequency.value = frequency;
const currentTime = audioContext.currentTime;
gainNode.gain.setValueAtTime(volume, currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, currentTime + duration);
oscillator.start(currentTime);
oscillator.stop(currentTime + duration);
} catch (e) {
console.log('音效播放失败:', e);
}
}
// 播放发射音效
function playShootSound() {
playSound(400, 0.1, 'square', 0.2);
}
// 播放爆炸音效
function playExplosionSound() {
const baseFreq = 300;
playSound(baseFreq, 0.2, 'sawtooth', 0.4);
setTimeout(() => playSound(baseFreq * 0.5, 0.15, 'sawtooth', 0.3), 50);
}
// 播放击中音效
function playHitSound() {
playSound(600, 0.1, 'sine', 0.25);
}
// 播放反弹音效
function playBounceSound() {
playSound(200, 0.05, 'square', 0.15);
}
// 创建爆炸特效
function createExplosion(x, y, size = 50) {
const explosion = {
x: x,
y: y,
emoji: '💥',
size: size,
life: 0.8, // 持续时间(秒)
maxLife: 0.8,
scale: 0,
rotation: Math.random() * Math.PI * 2
};
explosions.push(explosion);
// 创建粒子特效
for (let i = 0; i < 12; i++) {
particles.push({
x: x,
y: y,
vx: (Math.random() - 0.5) * 300,
vy: (Math.random() - 0.5) * 300,
life: 0.6,
maxLife: 0.6,
size: Math.random() * 5 + 2,
color: `hsl(${Math.random() * 60 + 10}, 100%, 60%)`
});
}
playExplosionSound();
}
// 更新爆炸特效
function updateExplosions(deltaTime) {
for (let i = explosions.length - 1; i >= 0; i--) {
const exp = explosions[i];
exp.life -= deltaTime;
exp.scale = 1 - (exp.life / exp.maxLife);
exp.rotation += deltaTime * 5;
if (exp.life <= 0) {
explosions.splice(i, 1);
}
}
// 更新粒子
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= deltaTime;
p.x += p.vx * deltaTime;
p.y += p.vy * deltaTime;
p.vx *= 0.95;
p.vy *= 0.95;
if (p.life <= 0) {
particles.splice(i, 1);
}
}
}
// 绘制爆炸特效
function drawExplosions() {
explosions.forEach(exp => {
const alpha = exp.life / exp.maxLife;
const size = exp.size * (1 + exp.scale * 1.5);
ctx.save();
ctx.globalAlpha = alpha;
ctx.translate(exp.x, exp.y);
ctx.rotate(exp.rotation);
ctx.font = `${size}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowBlur = 20;
ctx.shadowColor = '#ff4444';
ctx.fillText(exp.emoji, 0, 0);
ctx.restore();
});
// 绘制粒子
particles.forEach(p => {
const alpha = p.life / p.maxLife;
ctx.save();
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
}
// 初始化游戏
function initGame() {
// 清空数组
balls = [];
targets = [];
explosions = [];
particles = [];
// 重置状态
score = 0;
ballsRemaining = MAX_BALLS;
combo = 0;
maxCombo = 0;
gameOver = false;
launcherAngle = -Math.PI / 2;
// 生成目标方块
generateTargets();
updateUI();
}
// 生成目标方块
function generateTargets() {
targets = [];
targetCount = 0;
// 生成3行目标
const rows = 3;
const cols = 6;
const spacingX = (CANVAS_WIDTH - cols * TARGET_SIZE) / (cols + 1);
const startY = 80;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
targets.push({
x: spacingX + col * (TARGET_SIZE + spacingX) + TARGET_SIZE / 2,
y: startY + row * (TARGET_SIZE + 20),
width: TARGET_SIZE,
height: TARGET_SIZE,
color: `hsl(${row * 60 + col * 10}, 70%, 50%)`,
hit: false
});
targetCount++;
}
}
}
// 发射弹球
function shootBall() {
if (ballsRemaining <= 0 || !gameRunning || gamePaused || gameOver) return;
const ball = {
x: LAUNCHER_X,
y: LAUNCHER_Y,
vx: Math.cos(launcherAngle) * BALL_SPEED,
vy: Math.sin(launcherAngle) * BALL_SPEED,
radius: BALL_RADIUS,
active: true
};
balls.push(ball);
ballsRemaining--;
playShootSound();
updateUI();
// 检查游戏结束
if (ballsRemaining === 0 && balls.length === 0) {
setTimeout(() => {
if (targetCount > 0) {
endGame();
}
}, 2000);
}
}
// 更新弹球物理
function updateBalls(deltaTime) {
for (let i = balls.length - 1; i >= 0; i--) {
const ball = balls[i];
if (!ball.active) continue;
// 检查弹球数据有效性
if (!isFinite(ball.x) || !isFinite(ball.y) || !isFinite(ball.vx) || !isFinite(ball.vy)) {
ball.active = false;
continue;
}
// 应用重力
ball.vy += GRAVITY * deltaTime * 60; // 转换为每帧
// 更新位置
ball.x += ball.vx * deltaTime * 60;
ball.y += ball.vy * deltaTime * 60;
// 应用摩擦力
ball.vx *= Math.pow(FRICTION, deltaTime * 60);
ball.vy *= Math.pow(FRICTION, deltaTime * 60);
// 再次检查位置有效性(防止计算后变成NaN)
if (!isFinite(ball.x) || !isFinite(ball.y)) {
ball.active = false;
continue;
}
// 边界碰撞检测和反弹
// 左右侧壁:使用更强的反弹力度(增大50%)
if (ball.x - ball.radius <= 0) {
ball.x = ball.radius;
// 确保反弹后速度足够大,增加弹力感
const newVx = -ball.vx * SIDE_BOUNCE_DAMPING;
// 最小反弹速度增大50%(从2增加到3)
ball.vx = Math.abs(newVx) < SIDE_MIN_BOUNCE_SPEED ? (newVx > 0 ? SIDE_MIN_BOUNCE_SPEED : -SIDE_MIN_BOUNCE_SPEED) : newVx;
playBounceSound();
} else if (ball.x + ball.radius >= CANVAS_WIDTH) {
ball.x = CANVAS_WIDTH - ball.radius;
const newVx = -ball.vx * SIDE_BOUNCE_DAMPING;
// 最小反弹速度增大50%(从2增加到3)
ball.vx = Math.abs(newVx) < SIDE_MIN_BOUNCE_SPEED ? (newVx > 0 ? SIDE_MIN_BOUNCE_SPEED : -SIDE_MIN_BOUNCE_SPEED) : newVx;
playBounceSound();
}
// 上下边界:使用较小的反弹力度(受重力影响)
if (ball.y - ball.radius <= 0) {
ball.y = ball.radius;
ball.vy = -ball.vy * BOUNCE_DAMPING;
playBounceSound();
} else if (ball.y + ball.radius >= CANVAS_HEIGHT) {
ball.y = CANVAS_HEIGHT - ball.radius;
ball.vy = -ball.vy * BOUNCE_DAMPING;
playBounceSound();
}
// 检查与目标的碰撞
for (let j = targets.length - 1; j >= 0; j--) {
const target = targets[j];
if (target.hit) continue;
// 圆形与矩形碰撞检测
const closestX = Math.max(target.x - target.width / 2, Math.min(ball.x, target.x + target.width / 2));
const closestY = Math.max(target.y - target.height / 2, Math.min(ball.y, target.y + target.height / 2));
const dx = ball.x - closestX;
const dy = ball.y - closestY;
const distance = Math.sqrt(dx * dx + dy * dy);
// 防止distance为0导致NaN
if (distance < ball.radius && distance > 0.001) {
// 碰撞发生
target.hit = true;
targetCount--;
createExplosion(target.x, target.y, 60);
score += 100;
combo++;
maxCombo = Math.max(maxCombo, combo);
playHitSound();
// 移除目标
targets.splice(j, 1);
// 给弹球一个反弹力(增强反弹力度,防止卡住)
const normalX = dx / distance;
const normalY = dy / distance;
const dot = ball.vx * normalX + ball.vy * normalY;
// 计算反弹速度(增加反弹系数,从0.5提升到0.8)
let newVx = ball.vx - 2 * dot * normalX * 0.8;
let newVy = ball.vy - 2 * dot * normalY * 0.8;
// 检查计算结果是否有效
if (!isFinite(newVx)) newVx = ball.vx * -0.8; // 如果无效,简单反转
if (!isFinite(newVy)) newVy = ball.vy * -0.8;
// 确保反弹后速度足够大,防止卡住
const minBounceSpeed = 3;
const speed = Math.sqrt(newVx * newVx + newVy * newVy);
if (!isFinite(speed) || speed < minBounceSpeed) {
// 如果速度太小或无效,给一个最小反弹速度
const angle = Math.atan2(newVy || 0, newVx || 0);
if (!isFinite(angle)) {
// 如果角度也无效,使用默认向下角度
newVx = 0;
newVy = minBounceSpeed;
} else {
newVx = Math.cos(angle) * minBounceSpeed;
newVy = Math.sin(angle) * minBounceSpeed;
}
}
ball.vx = newVx;
ball.vy = newVy;
// 将弹球从目标位置推开,避免卡在目标内部
const pushDistance = ball.radius - distance + 2;
if (isFinite(pushDistance) && pushDistance > 0) {
ball.x += normalX * pushDistance;
ball.y += normalY * pushDistance;
}
// 检查是否所有目标都被击毁
if (targetCount === 0) {
setTimeout(() => {
endGame();
}, 1000);
}
break;
}
}
// 移除速度过小的弹球(静止或卡住)
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
// 如果速度很小,可能是卡住了
if (speed < 0.5) {
// 在底部区域,标记为不活跃
if (ball.y > CANVAS_HEIGHT - 100) {
ball.active = false;
}
// 在上方区域且速度极小,可能是卡在目标区域,给一个向下的推力
else if (ball.y < 250 && speed < 0.3) {
ball.vy += GRAVITY * 3; // 增加向下的力,帮助弹球下落
}
}
// 移除不活跃的弹球
if (!ball.active || ball.y > CANVAS_HEIGHT + 50) {
balls.splice(i, 1);
combo = 0; // 重置连击
// 检查游戏结束
if (ballsRemaining === 0 && balls.length === 0 && targetCount > 0) {
setTimeout(() => {
endGame();
}, 500);
}
}
}
}
// 绘制发射器
function drawLauncher() {
ctx.save();
ctx.strokeStyle = '#00d4ff';
ctx.lineWidth = 3;
ctx.lineCap = 'round';
// 绘制发射器底座
ctx.beginPath();
ctx.arc(LAUNCHER_X, LAUNCHER_Y, 15, 0, Math.PI * 2);
ctx.stroke();
// 绘制瞄准线
const aimLength = 50;
const aimX = LAUNCHER_X + Math.cos(launcherAngle) * aimLength;
const aimY = LAUNCHER_Y + Math.sin(launcherAngle) * aimLength;
ctx.beginPath();
ctx.moveTo(LAUNCHER_X, LAUNCHER_Y);
ctx.lineTo(aimX, aimY);
ctx.stroke();
// 绘制发射器图标
ctx.font = '20px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('🎯', LAUNCHER_X, LAUNCHER_Y);
ctx.restore();
}
// 绘制弹球
function drawBalls() {
balls.forEach(ball => {
if (!ball.active) return;
// 检查弹球位置是否有效(防止NaN或Infinity)
if (!isFinite(ball.x) || !isFinite(ball.y) || !isFinite(ball.radius)) {
ball.active = false; // 标记为不活跃,稍后会被移除
return;
}
ctx.save();
// 绘制弹球阴影
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.beginPath();
ctx.arc(ball.x + 2, ball.y + 2, ball.radius, 0, Math.PI * 2);
ctx.fill();
// 绘制弹球
const gradient = ctx.createRadialGradient(
ball.x - ball.radius * 0.3,
ball.y - ball.radius * 0.3,
0,
ball.x,
ball.y,
ball.radius
);
gradient.addColorStop(0, '#ffffff');
gradient.addColorStop(1, '#00d4ff');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fill();
// 绘制高光
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
ctx.beginPath();
ctx.arc(ball.x - ball.radius * 0.3, ball.y - ball.radius * 0.3, ball.radius * 0.4, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
}
// 绘制目标
function drawTargets() {
targets.forEach(target => {
if (target.hit) return;
ctx.save();
// 绘制阴影
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.fillRect(
target.x - target.width / 2 + 2,
target.y - target.height / 2 + 2,
target.width,
target.height
);
// 绘制目标
ctx.fillStyle = target.color;
ctx.fillRect(
target.x - target.width / 2,
target.y - target.height / 2,
target.width,
target.height
);
// 绘制边框
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.strokeRect(
target.x - target.width / 2,
target.y - target.height / 2,
target.width,
target.height
);
ctx.restore();
});
}
// 绘制背景网格
function drawBackground() {
ctx.fillStyle = '#000011';
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// 绘制网格
ctx.strokeStyle = 'rgba(0, 212, 255, 0.1)';
ctx.lineWidth = 1;
for (let x = 0; x < CANVAS_WIDTH; x += 40) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, CANVAS_HEIGHT);
ctx.stroke();
}
for (let y = 0; y < CANVAS_HEIGHT; y += 40) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(CANVAS_WIDTH, y);
ctx.stroke();
}
}
// 更新UI
function updateUI() {
document.getElementById('score').textContent = score;
document.getElementById('balls').textContent = ballsRemaining;
document.getElementById('targets').textContent = targetCount;
document.getElementById('combo').textContent = combo;
}
// 游戏循环
let lastTime = 0;
function update(currentTime) {
if (!gameRunning || gamePaused || gameOver) {
requestAnimationFrame(update);
return;
}
const deltaTime = (currentTime - lastTime) / 1000; // 转换为秒
lastTime = currentTime;
if (deltaTime > 0.1) return; // 防止时间跳跃过大
// 更新弹球物理
updateBalls(deltaTime);
// 更新特效
updateExplosions(deltaTime);
requestAnimationFrame(update);
}
function draw() {
if (!gameRunning || gamePaused) {
requestAnimationFrame(draw);
return;
}
// 清空画布
drawBackground();
// 绘制目标
drawTargets();
// 绘制弹球
drawBalls();
// 绘制发射器
drawLauncher();
// 绘制特效
drawExplosions();
requestAnimationFrame(draw);
}
// 开始游戏
function startGame() {
gameRunning = true;
gamePaused = false;
gameOver = false;
lastTime = performance.now();
initGame();
startBackgroundMusic();
document.getElementById('start-screen').classList.remove('active');
document.getElementById('game-over').classList.remove('active');
document.getElementById('pause-overlay').classList.remove('active');
requestAnimationFrame(update);
requestAnimationFrame(draw);
}
// 结束游戏
function endGame() {
gameRunning = false;
gameOver = true;
stopBackgroundMusic();
document.getElementById('final-score').textContent = score;
document.getElementById('final-balls').textContent = ballsRemaining;
document.getElementById('final-combo').textContent = maxCombo;
document.getElementById('game-over').classList.add('active');
}
// 暂停/继续游戏
function togglePause() {
if (!gameRunning || gameOver) return;
gamePaused = !gamePaused;
if (gamePaused) {
document.getElementById('pause-overlay').classList.add('active');
} else {
document.getElementById('pause-overlay').classList.remove('active');
lastTime = performance.now();
requestAnimationFrame(update);
requestAnimationFrame(draw);
}
}
// 事件监听
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top;
// 计算发射器角度
const dx = mouseX - LAUNCHER_X;
const dy = mouseY - LAUNCHER_Y;
launcherAngle = Math.atan2(dy, dx);
// 限制角度范围(不能向下发射,允许更垂直向上)
if (launcherAngle > -Math.PI / 8) {
launcherAngle = -Math.PI / 8; // 允许更垂直向上(约-22.5度)
} else if (launcherAngle < -Math.PI * 7 / 8) {
launcherAngle = -Math.PI * 7 / 8; // 允许更垂直向上(约-157.5度)
}
});
canvas.addEventListener('click', (e) => {
if (gameRunning && !gamePaused && !gameOver) {
shootBall();
}
});
document.addEventListener('keydown', (e) => {
keys[e.key] = true;
if (e.key === ' ' && gameRunning && !gamePaused && !gameOver) {
shootBall();
}
if (e.key === 'p' || e.key === 'P') {
togglePause();
}
if (e.key === 'r' || e.key === 'R') {
if (gameOver || !gameRunning) {
startGame();
}
}
// 键盘调整角度
if (e.key === 'a' || e.key === 'A' || e.key === 'ArrowLeft') {
launcherAngle -= 0.1;
if (launcherAngle < -Math.PI * 7 / 8) {
launcherAngle = -Math.PI * 7 / 8;
}
}
if (e.key === 'd' || e.key === 'D' || e.key === 'ArrowRight') {
launcherAngle += 0.1;
if (launcherAngle > -Math.PI / 8) {
launcherAngle = -Math.PI / 8;
}
}
});
document.addEventListener('keyup', (e) => {
keys[e.key] = false;
});
// UI按钮事件
document.getElementById('start-btn').addEventListener('click', () => {
startGame();
});
document.getElementById('restart-btn').addEventListener('click', () => {
startGame();
});
document.getElementById('pause-btn').addEventListener('click', () => {
togglePause();
});
document.getElementById('resume-btn').addEventListener('click', () => {
togglePause();
});
document.getElementById('new-game-btn').addEventListener('click', () => {
startGame();
});
// 初始化
initAudioContext();
document.getElementById('start-screen').classList.add('active');
------------ ⬆️·`正文结束`·⬆️------------
到此这篇文章就介绍到这了,更多精彩内容请关注本人以前的文章或继续浏览下面的文章,创作不易,如果能帮助到大家,希望大家多多支持宝码香车~💕,若转载本文,一定注明本文链接。

更多专栏订阅推荐:
💕 vue
✈️ Electron
⭐️ js
📝 字符串