(文后附完整代码)html+css+javascript 弹球射击游戏项目分析

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

(文后附完整代码)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类控制显示/隐藏:

  1. 开始界面start-screen):游戏未开始时显示
  2. 暂停遮罩pause-overlay):游戏暂停时覆盖
  3. 结束界面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-widthflex实现自适应
  • 居中布局: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 难度曲线

游戏流程

  1. 初期:10个弹球,充足的尝试机会
  2. 中期:物理反弹增加策略深度
  3. 后期:弹球用尽后需要精准瞄准

平衡设计

  • 完美弹性的左右边界允许弹球反弹多次
  • 有限的弹球数量鼓励精准瞄准
  • 连击系统奖励连续命中

📚 九、用户体验设计

📘 9.1 视觉反馈

即时反馈

  • 击中目标时的爆炸特效
  • 弹球碰撞时的音效
  • 得分和连击数的实时更新

状态提示

  • 暂停界面清晰的继续按钮
  • 游戏结束时的最终成绩展示
  • 开始界面的操作说明

📘 9.2 交互设计

操作方式

  • 鼠标瞄准(直观)
  • 键盘辅助(精确控制)
  • 多种发射方式(左键/空格)

容错设计

  • 角度限制防止误操作
  • 暂停功能允许中断
  • 重新开始按钮随时可用

📘 9.3 音效反馈

音频提示

  • 发射音效:确认操作
  • 击中音效:成功反馈
  • 爆炸音效:奖励反馈
  • 反弹音效:物理反馈

📚 十、总结与技术亮点

📘 10.1 技术亮点

  1. 自定义物理引擎:完整的重力、碰撞、反弹系统
  2. 双循环架构:更新与渲染分离,确保物理稳定性
  3. Web Audio API:程序化音乐生成和音效合成
  4. 特效系统:Emoji爆炸 + 粒子系统的视觉冲击
  5. 响应式设计:Flexbox布局适配不同屏幕
  6. 状态机管理:清晰的游戏状态转换逻辑
  7. 健壮性保护:全面的NaN/Infinity检测,防止数值异常导致游戏崩溃

📘 10.2 设计优势

  • 代码简洁:780行实现完整游戏
  • 性能优秀:60fps流畅运行
  • 体验丰富:视觉、听觉、触觉反馈齐全
  • 易于扩展:模块化设计支持功能添加

📘 10.3 可扩展方向

  1. 难度系统:多关卡、Boss战
  2. 道具系统:加速、分裂、穿透等特殊弹球
  3. 排行榜:本地存储或云端排名
  4. 多人模式:分屏或联网对战
  5. 皮肤系统:自定义弹球和目标外观
  6. 关卡编辑器:用户创建和分享关卡

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

📘 项目目录

📘 项目代码

📖 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');

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


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


更多专栏订阅推荐:

👍 html+css+js 绚丽效果

💕 vue

✈️ Electron

⭐️ js

📝 字符串

✍️ 时间对象(Date())操作

相关推荐
qq_459558692 小时前
使用DrissionPage打开Edge
前端·edge
二哈喇子!10 小时前
BOM模型
开发语言·前端·javascript·bom
二哈喇子!10 小时前
Vue2 监听器 watcher
前端·javascript·vue.js
上海云盾商务经理杨杨10 小时前
2026游戏盾深度解析:从被动防御到智能作战,构建DDoS免疫堡垒
网络·游戏·ddos
yanyu-yaya10 小时前
前端面试题
前端·面试·前端框架
二哈喇子!11 小时前
使用NVM下载Node.js管理多版本
前端·npm·node.js
GGGG寄了11 小时前
HTML——文本标签
开发语言·前端·html
摘星编程12 小时前
在OpenHarmony上用React Native:ActionSheet确认删除
javascript·react native·react.js
2501_9445215912 小时前
Flutter for OpenHarmony 微动漫App实战:推荐动漫实现
android·开发语言·前端·javascript·flutter·ecmascript