HTML5 Canvas 从入门到实战:手把手教你打造一款"打飞机"小游戏

🎨 HTML5 Canvas 从入门到实战:手把手教你打造一款"打飞机"小游戏

阅读提示: 本文从零开始讲解 Canvas 的核心概念,通过「基础绘图 → 动画原理 → 完整游戏」三个递进阶段,带你彻底掌握 Canvas 开发。文章包含大量可运行的代码示例和详细的原理讲解,建议收藏后配合代码实操。


📖 目录

  • [一、Canvas 是什么?](#一、Canvas 是什么? "#%E4%B8%80canvas-%E6%98%AF%E4%BB%80%E4%B9%88")
  • [二、Canvas 基础入门:绘制你的第一幅画](#二、Canvas 基础入门:绘制你的第一幅画 "#%E4%BA%8Ccanvas-%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8%E7%BB%98%E5%88%B6%E4%BD%A0%E7%9A%84%E7%AC%AC%E4%B8%80%E5%B9%85%E7%94%BB")
    • [2.1 Canvas 元素与渲染上下文](#2.1 Canvas 元素与渲染上下文 "#21-canvas-%E5%85%83%E7%B4%A0%E4%B8%8E%E6%B8%B2%E6%9F%93%E4%B8%8A%E4%B8%8B%E6%96%87")
    • [2.2 填充矩形 fillRect](#2.2 填充矩形 fillRect "#22-%E5%A1%AB%E5%85%85%E7%9F%A9%E5%BD%A2-fillrect")
    • [2.3 描边矩形 strokeRect](#2.3 描边矩形 strokeRect "#23-%E6%8F%8F%E8%BE%B9%E7%9F%A9%E5%BD%A2-strokerect")
    • [2.4 清除矩形 clearRect](#2.4 清除矩形 clearRect "#24-%E6%B8%85%E9%99%A4%E7%9F%A9%E5%BD%A2-clearrect")
    • [2.5 Canvas 坐标系详解](#2.5 Canvas 坐标系详解 "#25-canvas-%E5%9D%90%E6%A0%87%E7%B3%BB%E8%AF%A6%E8%A7%A3")
  • [三、Canvas 动画原理:让画面动起来](#三、Canvas 动画原理:让画面动起来 "#%E4%B8%89canvas-%E5%8A%A8%E7%94%BB%E5%8E%9F%E7%90%86%E8%AE%A9%E7%94%BB%E9%9D%A2%E5%8A%A8%E8%B5%B7%E6%9D%A5")
    • [3.1 requestAnimationFrame ------ 动画的心脏](#3.1 requestAnimationFrame —— 动画的心脏 "#31-requestanimationframe--%E5%8A%A8%E7%94%BB%E7%9A%84%E5%BF%83%E8%84%8F")
    • [3.2 实现一个平滑移动的矩形](#3.2 实现一个平滑移动的矩形 "#32-%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E5%B9%B3%E6%BB%91%E7%A7%BB%E5%8A%A8%E7%9A%84%E7%9F%A9%E5%BD%A2")
    • [3.3 动画循环的完整流程](#3.3 动画循环的完整流程 "#33-%E5%8A%A8%E7%94%BB%E5%BE%AA%E7%8E%AF%E7%9A%84%E5%AE%8C%E6%95%B4%E6%B5%81%E7%A8%8B")
  • [四、实战项目:打飞机小游戏 🚀](#四、实战项目:打飞机小游戏 🚀 "#%E5%9B%9B%E5%AE%9E%E6%88%98%E9%A1%B9%E7%9B%AE%E6%89%93%E9%A3%9E%E6%9C%BA%E5%B0%8F%E6%B8%B8%E6%88%8F-")
    • [4.1 项目架构概览](#4.1 项目架构概览 "#41-%E9%A1%B9%E7%9B%AE%E6%9E%B6%E6%9E%84%E6%A6%82%E8%A7%88")
    • [4.2 游戏核心设计](#4.2 游戏核心设计 "#42-%E6%B8%B8%E6%88%8F%E6%A0%B8%E5%BF%83%E8%AE%BE%E8%AE%A1")
    • [4.3 玩家飞机绘制](#4.3 玩家飞机绘制 "#43-%E7%8E%A9%E5%AE%B6%E9%A3%9E%E6%9C%BA%E7%BB%98%E5%88%B6")
    • [4.4 子弹系统](#4.4 子弹系统 "#44-%E5%AD%90%E5%BC%B9%E7%B3%BB%E7%BB%9F")
    • [4.5 敌机生成与难度递增](#4.5 敌机生成与难度递增 "#45-%E6%95%8C%E6%9C%BA%E7%94%9F%E6%88%90%E4%B8%8E%E9%9A%BE%E5%BA%A6%E9%80%92%E5%A2%9E")
    • [4.6 碰撞检测算法](#4.6 碰撞检测算法 "#46-%E7%A2%B0%E6%92%9E%E6%A3%80%E6%B5%8B%E7%AE%97%E6%B3%95")
    • [4.7 粒子爆炸特效](#4.7 粒子爆炸特效 "#47-%E7%B2%92%E5%AD%90%E7%88%86%E7%82%B8%E7%89%B9%E6%95%88")
    • [4.8 星空背景](#4.8 星空背景 "#48-%E6%98%9F%E7%A9%BA%E8%83%8C%E6%99%AF")
    • [4.9 HUD 与游戏状态管理](#4.9 HUD 与游戏状态管理 "#49-hud-%E4%B8%8E%E6%B8%B8%E6%88%8F%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86")
  • 五、关键技术点深度解析
  • 六、项目运行与调试
  • 七、总结与进阶方向

一、Canvas 是什么?

<canvas> 是 HTML5 引入的核心特性之一,它提供了一块位图渲染区域,允许开发者通过 JavaScript 动态绘制图形、图像、动画甚至游戏。

简单来说,Canvas 就像一块画布------你拿到画笔(JavaScript API),就可以在上面自由作画。

Canvas 的核心特点

特性 说明
像素级操作 可以直接操作每一个像素,实现图像滤镜等高级效果
即时模式 绘制后不保留图形对象,只保留最终的像素结果
高性能 利用 GPU 加速渲染,适合游戏和动画场景
2D/3D 支持 通过 '2d''webgl' 上下文分别支持 2D 和 3D 渲染

即时模式 vs 保留模式: Canvas 是即时模式渲染------画完一笔,系统不会记住"这是一个矩形",只会记住像素颜色。这与 SVG 的保留模式(每个图形都是一个可操作的 DOM 节点)截然不同。


二、Canvas 基础入门:绘制你的第一幅画

2.1 Canvas 元素与渲染上下文

让我们从最基础的 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>HTML5 Canvas 入门</title>
</head>
<body>
    <!-- 
      width/height 属性定义画布的实际像素尺寸
      注意:不要用 CSS 的 width/height 来设置,那只会拉伸画布
    -->
    <canvas id="myCanvas" width="600" height="400" 
            style="border: 1px solid #333;">
        你的浏览器不支持Canvas(旧浏览器会显示这段文字)
    </canvas>
    
    <script>
        // ① 获取画布元素
        const canvas = document.querySelector('#myCanvas');
        
        // ② 获取 2D 渲染上下文 ------ 这是所有绘制的入口
        const ctx = canvas.getContext('2d');
        
        // ③ 接下来就可以使用 ctx 进行各种绘制操作了
    </script>
</body>
</html>
🔑 关键概念:渲染上下文(Rendering Context)

getContext('2d') 返回一个 CanvasRenderingContext2D 对象,它是所有 2D 绘制 API 的集合。你可以把它理解为:

  • 画布(canvas) = 画板
  • 上下文(ctx) = 画笔 + 调色板 + 所有绘画工具

⚠️ 常见误区: 不要用 CSS 的 width/height 来设置 Canvas 尺寸。Canvas 有两个尺寸 ------元素自身的 width/height 属性(定义像素分辨率)和 CSS 的 width/height(定义显示大小)。用 CSS 拉伸会导致图像模糊。


2.2 填充矩形 fillRect

fillRect(x, y, width, height) 绘制一个填充矩形:

javascript 复制代码
const canvas = document.querySelector('#myCanvas');
const ctx = canvas.getContext('2d');

// 设置填充颜色
ctx.fillStyle = '#4299e1';     // 类似于 Tailwind 的 blue-400
// 绘制填充矩形:(x, y, 宽度, 高度)
ctx.fillRect(20, 20, 100, 80); // 从 (20,20) 开始画一个 100×80 的蓝色矩形

fillStyle 支持所有 CSS 颜色格式:

  • 颜色名:'red', 'blue'
  • 十六进制:'#4299e1'
  • RGB/RGBA:'rgb(66, 153, 225)', 'rgba(66, 153, 225, 0.5)'
  • HSL:'hsl(210, 75%, 57%)'

2.3 描边矩形 strokeRect

strokeRect(x, y, width, height) 绘制一个空心矩形框:

javascript 复制代码
// 设置描边颜色
ctx.strokeStyle = '#f56565';    // 红色边框
// 设置线条宽度(单位:像素)
ctx.lineWidth = 4;
// 绘制描边矩形
ctx.strokeRect(150, 20, 100, 80);

描边相关的属性:

属性 说明 示例值
strokeStyle 线条颜色 '#f56565'
lineWidth 线条宽度 4
lineCap 线条端点样式 'butt' / 'round' / 'square'
lineJoin 线条连接处样式 'miter' / 'round' / 'bevel'

2.4 清除矩形 clearRect

clearRect(x, y, width, height) 擦除指定区域,使其变为透明:

javascript 复制代码
ctx.clearRect(50, 50, 40, 30);  // 在 (50,50) 位置擦除一块 40×30 的区域

这个 API 在动画中至关重要------每一帧都需要先清除上一帧的画面,再绘制新内容,否则画面会不断叠加。


2.5 Canvas 坐标系详解

Canvas 使用标准的 二维笛卡尔坐标系

scss 复制代码
(0,0) ──────────────── X 轴(向右为正)→
  │
  │   * (x, y)
  │
  ↓
 Y 轴(向下为正)

记住一个关键点: Canvas 的 Y 轴方向是向下的 ,这与数学中常见的坐标系相反。坐标原点 (0, 0) 在画布的左上角


📝 完整示例代码(基础篇)

结合以上所有知识点,以下是 1.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>HTML5 Canvas 基础绘图</title>
</head> 
<body>
    <canvas id="myCanvas" width="600" height="400" 
            style="border: 1px solid #333;">
        你的浏览器不支持Canvas(旧浏览器会显示这段文字)
    </canvas>
    
    <script>
        // 1. 获取画布元素
        const canvas = document.querySelector('#myCanvas');
        
        // 2. 获取 2D 渲染上下文
        const ctx = canvas.getContext('2d');
        
        // 3. 填充矩形 ------ 蓝色实心方块
        ctx.fillStyle = '#4299e1';
        ctx.fillRect(20, 20, 100, 80);
        
        // 4. 描边矩形 ------ 红色空心框
        ctx.strokeStyle = '#f56565';
        ctx.lineWidth = 4;
        ctx.strokeRect(150, 20, 100, 80);
        
        // 5. 清除矩形 ------ 擦除指定区域
        ctx.clearRect(50, 50, 40, 30);
    </script>
</body>
</html>

🎯 运行效果: 画布上会出现一个蓝色填充矩形、一个红色描边矩形,以及被擦除的一块透明区域。


三、Canvas 动画原理:让画面动起来

静态绘图只是第一步,Canvas 真正的威力在于动画。

3.1 requestAnimationFrame ------ 动画的心脏

在 Canvas 中实现动画,核心是不断重复"清除 → 绘制"这一循环 。而驱动这个循环的最佳工具就是 requestAnimationFrame

为什么不用 setInterval?
对比维度 setInterval requestAnimationFrame
帧率 固定间隔,无法与屏幕刷新同步 自动匹配屏幕刷新率(通常 60fps)
性能 页面不可见时仍在执行 页面不可见时自动暂停,节省资源
流畅度 可能掉帧或撕裂 浏览器优化,画面更流畅
用途 定时任务 动画专用
javascript 复制代码
// ❌ 不推荐:setInterval 动画
setInterval(() => {
    // 绘制逻辑
}, 16); // 约 60fps

// ✅ 推荐:requestAnimationFrame 动画
function animate() {
    // 绘制逻辑
    requestAnimationFrame(animate); // 递归调用,形成循环
}
animate(); // 启动循环

3.2 实现一个平滑移动的矩形

以下是 2.html 的完整实现,演示了 Canvas 动画的核心流程:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canvas 动画入门</title>
</head>
<body>
    <canvas id="myCanvas" width="600" height="400" 
            style="border: 1px solid #333;">
        你的浏览器不支持Canvas(旧浏览器会显示这段文字)
    </canvas>
    
    <script>
        const canvas = document.querySelector('#myCanvas');
        const ctx = canvas.getContext('2d');
        
        // ── 动画参数 ──
        let x = 20;              // 矩形的初始 x 坐标
        const y = 20;            // 矩形的 y 坐标(固定不变)
        const width = 100;       // 矩形宽度
        const height = 80;       // 矩形高度
        const speed = 3;         // 每帧移动的像素数
        
        // ── 动画循环 ──
        function animate() {
            // ① 清除整个画布(这是最关键的一步!)
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            
            // ② 在新位置绘制矩形
            ctx.fillStyle = '#4299e1';
            ctx.fillRect(x, y, width, height);
            
            // ③ 更新位置
            x += speed;
            
            // ④ 边界处理:移出屏幕后从左侧重新进入
            if (x > canvas.width) {
                x = -width;
            }
            
            // ⑤ 请求下一帧
            requestAnimationFrame(animate);
        }
        
        // 启动动画
        animate();
    </script>
</body>
</html>

3.3 动画循环的完整流程

Canvas 动画遵循一个标准循环,我称之为 动画五步法

bash 复制代码
┌─────────────────────────────────────────────┐
│                                               │
│   ① clearRect  清除上一帧画面                 │
│        ↓                                      │
│   ② save/restore  保存/恢复绘制状态(可选)    │
│        ↓                                      │
│   ③ 绘制  绘制当前帧的所有内容                │
│        ↓                                      │
│   ④ 更新  更新所有对象的位置/状态             │
│        ↓                                      │
│   ⑤ requestAnimationFrame  请求下一帧         │
│        ↓                                      │
│   回到 ① ...无限循环                          │
│                                               │
└─────────────────────────────────────────────┘

⚠️ 为什么必须 clearRect? 如果不清除画布,每一帧的内容会叠加在上一帧之上,矩形就会变成一条长长的"拖尾"。可以试着注释掉 clearRect 那行看看效果------这是一个很有教学意义的实验。


四、实战项目:打飞机小游戏 🚀

掌握了基础绘图和动画原理后,我们来实现一个完整的打飞机小游戏。这是一个非常好的 Canvas 综合练习,涵盖了图形绘制、动画循环、碰撞检测、粒子特效、状态管理等核心技能。

4.1 项目架构概览

bash 复制代码
airplane/
├── index.html          # 游戏入口页面
├── package.json        # 项目配置(使用 Vite 构建)
├── vite.config.js      # Vite 配置
└── src/
    └── main.js         # 游戏核心逻辑(约 410 行)

我们使用 Vite 作为开发服务器------它提供热更新(HMR),修改代码后浏览器自动刷新,极大提升开发效率。

启动方式:

bash 复制代码
cd airplane
npm install
npm run dev

4.2 游戏核心设计

在动手写代码之前,让我们先理清整个游戏的架构设计:

sql 复制代码
┌──────────────────────────────────────────────────┐
│                   Game Loop (60fps)                 │
│                                                     │
│  ┌──────────┐  ┌──────────┐  ┌──────────────────┐ │
│  │ 输入处理  │→│ 游戏逻辑  │→│    渲染绘制       │ │
│  │ KeyBoard │  │ 碰撞检测  │  │ 飞机/子弹/敌机/UI │ │
│  └──────────┘  │ 难度递增  │  └──────────────────┘ │
│                └──────────┘                         │
│                                                     │
│  状态机:  RUNNING ←→ PAUSED → OVER                 │
│                ↕ (按R重置)                          │
└──────────────────────────────────────────────────┘
游戏状态机
javascript 复制代码
const STATE = { RUNNING: 0, PAUSED: 1, OVER: 2 };
let state = STATE.RUNNING;

使用简单的状态机模式管理三种游戏状态:

  • RUNNING:正常游戏进行中
  • PAUSED:游戏暂停
  • OVER:游戏结束,显示结算画面

4.3 玩家飞机绘制

玩家飞机采用纯 Canvas 路径绘制,不使用任何图片资源------这意味着零依赖,一个 HTML 文件就能跑起来。

javascript 复制代码
const PLAYER_W = 48;
const PLAYER_H = 56;
const PLAYER_SPEED = 5;

const player = {
    x: W / 2 - PLAYER_W / 2,   // 初始位于画布底部中央
    y: H - PLAYER_H - 30,
    w: PLAYER_W,
    h: PLAYER_H,
};

function drawPlayer() {
    const { x, y, w, h } = player;
    ctx.save();
    ctx.translate(x + w / 2, y + h / 2);  // 将坐标系原点移到飞机中心

    // ── 发光效果(shadow)──
    ctx.shadowColor = '#00ffcc';
    ctx.shadowBlur = 12;

    // ── 机身主体(六边形战斗机型)──
    ctx.fillStyle = '#00d4ff';
    ctx.beginPath();
    ctx.moveTo(0, -h / 2);           // 机头(上方尖端)
    ctx.lineTo(-w / 2, h / 2);       // 左下翼
    ctx.lineTo(-w / 6, h / 3);       // 左内收
    ctx.lineTo(0, h / 4 + 2);        // 尾部中心
    ctx.lineTo(w / 6, h / 3);        // 右内收
    ctx.lineTo(w / 2, h / 2);        // 右下翼
    ctx.closePath();
    ctx.fill();

    // ── 驾驶舱(椭圆形高光)──
    ctx.fillStyle = '#7effff';
    ctx.shadowBlur = 0;               // 驾驶舱不需要发光
    ctx.beginPath();
    ctx.ellipse(0, -6, 8, 12, 0, 0, Math.PI * 2);
    ctx.fill();

    // ── 引擎火焰(动态效果)──
    ctx.fillStyle = '#ff6600';
    ctx.shadowColor = '#ff4400';
    ctx.shadowBlur = 8;
    ctx.beginPath();
    ctx.moveTo(-7, h / 4 + 2);
    ctx.lineTo(0, h / 2 + 6 + Math.random() * 8);  // Math.random() 让火焰抖动!
    ctx.lineTo(7, h / 4 + 2);
    ctx.closePath();
    ctx.fill();

    ctx.shadowBlur = 0;
    ctx.restore();
}
🎨 绘制技巧分析
技巧 代码 效果
坐标平移 ctx.translate(x + w/2, y + h/2) 以飞机中心为原点绘制,简化坐标计算
状态保存/恢复 ctx.save() / ctx.restore() 隔离绘制状态,防止相互影响
发光阴影 shadowColor + shadowBlur 科幻风格的光晕效果
动态火焰 Math.random() * 8 引擎火焰随机抖动,更有真实感
多层绘制 机身 → 驾驶舱 → 火焰 从底到顶分层绘制,形成丰富细节

💡 核心思想: save()restore() 是 Canvas 最重要的状态管理方法。每次绘制独立的图形时,都应该包裹在这两者之间,这样修改 translateshadowBlur 等属性就不会影响到其他图形的绘制。


4.4 子弹系统

子弹系统涉及生成、移动、渲染、回收四个环节:

javascript 复制代码
const BULLET_W = 4;
const BULLET_H = 14;
const BULLET_SPEED = 8;
const BULLET_COOLDOWN = 12;  // 射击冷却帧数
let bullets = [];
let bulletCooldown = 0;

// ── 射击逻辑(在 handleInput 中)──
if (keys['Space'] && bulletCooldown <= 0) {
    bullets.push({
        x: player.x + player.w / 2 - BULLET_W / 2,  // 从飞机中心射出
        y: player.y - BULLET_H,                      // 从飞机顶部出现
        w: BULLET_W,
        h: BULLET_H,
    });
    bulletCooldown = BULLET_COOLDOWN;  // 重置冷却计数器
}

// ── 子弹更新(在 updateGame 中)──
for (const b of bullets) {
    b.y -= BULLET_SPEED;  // 子弹向上移动
}
bullets = bullets.filter(b => b.y + b.h > 0);  // 越界回收

// ── 子弹渲染 ──
function drawBullet(b) {
    ctx.save();
    ctx.shadowColor = '#ffdd00';
    ctx.shadowBlur = 6;

    // 渐变填充:白 → 黄 → 橙
    const grad = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
    grad.addColorStop(0, '#ffffff');
    grad.addColorStop(0.3, '#ffee00');
    grad.addColorStop(1, '#ff8800');
    ctx.fillStyle = grad;

    ctx.beginPath();
    ctx.roundRect(b.x, b.y, b.w, b.h, 2);  // 圆角矩形子弹
    ctx.fill();

    ctx.shadowBlur = 0;
    ctx.restore();
}
系统设计要点
  1. 冷却机制(Cooldown): 如果不加冷却限制,按住空格键时每帧都会生成一颗子弹(60发/秒),游戏会瞬间变得毫无挑战。12 帧的冷却意味着每秒最多发射 5 发。

  2. 对象池思想: 虽然这里用的是简单的数组 filter,但本质上是在复用内存------飞出去的子弹被过滤回收,新子弹加入数组。

  3. 循环遍历技巧: 碰撞检测时使用倒序遍历i = bullets.length - 1),这样在 splice 删除元素时不会影响后续索引。


4.5 敌机生成与难度递增

javascript 复制代码
const ENEMY_MIN_W = 30;
const ENEMY_MAX_W = 55;
let enemies = [];
let enemySpawnTimer = 0;
let enemySpawnInterval = 45;  // 初始每 45 帧生成一架敌机
let enemySpeed = 2.5;          // 初始速度

function spawnEnemy() {
    if (state !== STATE.RUNNING) return;

    const w = ENEMY_MIN_W + Math.random() * (ENEMY_MAX_W - ENEMY_MIN_W);
    const h = w * 1.1;
    enemies.push({
        x: Math.random() * (W - w),     // 随机水平位置
        y: -h,                           // 从屏幕顶端外出现
        w,
        h,
    });
}

// ── 难度递增(在 updateGame 中)──
enemySpawnTimer++;
if (enemySpawnTimer >= enemySpawnInterval) {
    enemySpawnTimer = 0;
    spawnEnemy();
    // 🎯 关键:根据分数动态调整难度
    enemySpawnInterval = Math.max(15, 45 - Math.floor(score / 100));
    enemySpeed = 2.5 + Math.floor(score / 80) * 0.5;
}
📈 难度曲线分析
分数范围 生成间隔 敌机速度 游戏体验
0-99 45 帧 2.5 px/frame 🌱 轻松入门
100-179 44→45 帧 3.0 px/frame 稍有压力
200-299 43→44 帧 3.5 px/frame 😰 开始紧张
500+ ≤40 帧 5.5+ px/frame 🔥 高强度
  • Math.max(15, ...) 保证生成间隔不会低于 15 帧(约每秒 4 架),防止难度无限增长导致不可玩。
  • 双重维度(生成速度 + 移动速度)同时递增,形成乘数效应,体验曲线更加丰富。

4.6 碰撞检测算法

游戏中最核心的算法之一------判断两个矩形是否相交:

javascript 复制代码
function rectCollide(a, b) {
    return (
        a.x < b.x + b.w &&      // a 的左边 < b 的右边
        a.x + a.w > b.x &&      // a 的右边 > b 的左边
        a.y < b.y + b.h &&      // a 的上边 < b 的下边
        a.y + a.h > b.y         // a 的下边 > b 的上边
    );
}

这个算法的本质是反向判断:如果两个矩形在 X 轴和 Y 轴上都有重叠,则它们相交。

markdown 复制代码
    X轴重叠:a_left < b_right AND a_right > b_left
    Y轴重叠:a_top  < b_bottom AND a_bottom > b_top
    ↓
    两者同时满足 → 碰撞!

碰撞检测在游戏循环中的应用:

javascript 复制代码
// 子弹 vs 敌机(双重倒序循环 + break 优化)
for (let i = bullets.length - 1; i >= 0; i--) {
    for (let j = enemies.length - 1; j >= 0; j--) {
        if (rectCollide(bullets[i], enemies[j])) {
            // 生成爆炸特效
            spawnExplosion(
                enemies[j].x + enemies[j].w / 2,
                enemies[j].y + enemies[j].h / 2,
                12, '#ff6600'
            );
            bullets.splice(i, 1);   // 移除子弹
            enemies.splice(j, 1);   // 移除敌机
            score += 10;            // 加分
            break;                  // 一颗子弹只能击中一架敌机
        }
    }
}

// 敌机 vs 玩家(简单遍历即可)
for (const e of enemies) {
    if (rectCollide(player, e)) {
        spawnExplosion(player.x + player.w / 2, player.y + player.h / 2, 30, '#00ff88');
        gameOver();
        return;  // 游戏结束,立即退出
    }
}

🔑 倒序遍历 + splice 是游戏开发的经典组合技,避免了正序遍历删除元素时的索引错位问题。


4.7 粒子爆炸特效

爆炸效果是游戏爽感的重要来源。我们用粒子系统来实现:

javascript 复制代码
let particles = [];

// ── 生成爆炸粒子 ──
function spawnExplosion(x, y, count, baseColor) {
    for (let i = 0; i < count; i++) {
        const angle = Math.random() * Math.PI * 2;     // 随机角度(0~360°)
        const speed = 1 + Math.random() * 4;            // 随机速度
        particles.push({
            x, y,                                        // 初始位置
            vx: Math.cos(angle) * speed,                 // X 方向速度
            vy: Math.sin(angle) * speed,                 // Y 方向速度
            size: 1.5 + Math.random() * 3,              // 粒子大小
            color: baseColor,                            // 粒子颜色
            life: 1,                                     // 生命值(1 = 完全可见)
            decay: 0.02 + Math.random() * 0.04,         // 衰减速度
        });
    }
}

// ── 更新粒子 ──
function updateParticles() {
    for (const p of particles) {
        p.x += p.vx;        // 匀速运动
        p.y += p.vy;
        p.life -= p.decay;   // 逐渐消失
    }
    particles = particles.filter(p => p.life > 0);  // 移除死亡粒子
}

// ── 绘制粒子 ──
function drawParticle(p) {
    ctx.save();
    ctx.globalAlpha = p.life;        // 透明度随生命值衰减
    ctx.fillStyle = p.color;
    ctx.shadowColor = p.color;
    ctx.shadowBlur = 4;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
    ctx.fill();
    ctx.shadowBlur = 0;
    ctx.restore();
}
粒子系统的生命周期
ini 复制代码
诞生(spawnExplosion)
  │  设定:位置、速度、大小、颜色、生命值
  ↓
更新(updateParticles)
  │  每帧:位置 += 速度,生命值 -= 衰减
  ↓
绘制(drawParticle)
  │  透明度 = 生命值,颜色渐淡
  ↓
死亡(life ≤ 0)
  │  被 filter 过滤回收
  ↓
消失

💡 通过改变 count(粒子数量)和 baseColor(基色),同一个 spawnExplosion 函数可以创建出不同风格的爆炸效果。敌机爆炸用橙色(#ff6600),玩家爆炸用青绿色(#00ff88)。


4.8 星空背景

一个静态的黑色背景太无聊了,我们用程序化生成的星空来营造太空氛围:

javascript 复制代码
function drawStars() {
    for (let i = 0; i < 60; i++) {
        // 使用质数 + 取模来生成"伪随机"但稳定的位置
        const sx = (i * 73 + 17) % W;
        const sy = ((i * 97 + 31) + (Date.now() * 0.02) % (H * 10)) % H;
        
        // 不同星星有不同亮度,增加层次感
        const brightness = 0.3 + (i % 3) * 0.2;
        
        ctx.fillStyle = `rgba(255, 255, 255, ${brightness})`;
        ctx.fillRect(sx, sy, 2, 2);  // 每个星星是一个 2×2 的小方块
    }
}
🎯 为什么不用 Math.random()?

在动画循环中使用 Math.random() 会导致每帧星星位置都在变------看起来像是闪烁干扰而不是星空。这里用 质数散列i * 73 + 17 等)来生成确定性的"随机"位置:

  • X 坐标:固定值,保证星星水平位置不变
  • Y 坐标 :加上 Date.now() * 0.02 的偏移,让星星缓慢向下流动
  • 亮度i % 3 产生 3 种不同亮度等级,模拟远近不同的星星

4.9 HUD 与游戏状态管理

HUD(抬头显示)
javascript 复制代码
function drawHUD() {
    ctx.font = 'bold 16px "Courier New"';
    ctx.textBaseline = 'top';

    // 当前分数(带发光)
    ctx.fillStyle = '#00ff88';
    ctx.shadowColor = '#00ff88';
    ctx.shadowBlur = 4;
    ctx.fillText(`SCORE: ${score}`, 14, 14);
    ctx.shadowBlur = 0;

    // 最高分
    ctx.fillStyle = '#888';
    ctx.font = '12px "Courier New"';
    ctx.fillText(`BEST: ${highScore}`, 14, 38);
}
游戏结束画面
javascript 复制代码
function drawGameOver() {
    // 半透明遮罩
    ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
    ctx.fillRect(0, 0, W, H);

    // 标题
    ctx.font = 'bold 42px "Courier New"';
    ctx.fillStyle = '#ff3344';
    ctx.shadowColor = '#ff3344';
    ctx.shadowBlur = 14;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText('GAME OVER', W / 2, H / 2 - 30);
    ctx.shadowBlur = 0;

    // 最终得分
    ctx.font = '18px "Courier New"';
    ctx.fillStyle = '#fff';
    ctx.fillText(`最终得分: ${score}`, W / 2, H / 2 + 30);

    // 操作提示
    ctx.fillStyle = '#aaa';
    ctx.font = '14px "Courier New"';
    ctx.fillText('按 R 键重新开始', W / 2, H / 2 + 65);

    // 重置文本对齐(避免影响后续绘制)
    ctx.textAlign = 'start';
    ctx.textBaseline = 'alphabetic';
}
持久化最高分
javascript 复制代码
// 读取历史最高分
let highScore = parseInt(localStorage.getItem('planeHighScore') || '0');

// 游戏结束时保存
function gameOver() {
    state = STATE.OVER;
    if (score > highScore) {
        highScore = score;
        localStorage.setItem('planeHighScore', highScore);  // 持久化到浏览器
    }
}

使用 localStorage 可以让最高分在刷新页面甚至关闭浏览器后依然保留,这是提升玩家留存率的小细节。


🎮 完整游戏循环

把所有模块串联起来的主循环:

javascript 复制代码
function gameLoop() {
    // ① 清除画布
    ctx.clearRect(0, 0, W, H);

    // ② 绘制背景
    drawStars();

    // ③ 处理输入 + 更新游戏状态
    handleInput();
    updateGame();

    // ④ 绘制所有游戏对象
    drawPlayer();
    for (const b of bullets) drawBullet(b);
    for (const e of enemies) drawEnemy(e);
    for (const p of particles) drawParticle(p);

    // ⑤ 绘制 UI
    drawHUD();
    if (state === STATE.OVER) {
        drawGameOver();
    }

    // ⑥ 请求下一帧
    requestAnimationFrame(gameLoop);
}

// 🚀 启动游戏
gameLoop();

五、关键技术点深度解析

5.1 Canvas 状态管理:save/restore

这是 Canvas 开发中最容易被忽视但又最重要的概念:

javascript 复制代码
// ❌ 错误示例:shadow 污染了后续绘制
ctx.shadowBlur = 20;
drawPlayer();     // 玩家有发光效果 ✓
drawEnemy();      // 敌机也有发光效果 ✗(本不想要)

// ✅ 正确示例:save/restore 隔离状态
ctx.save();
ctx.shadowBlur = 20;
drawPlayer();     // 只有玩家有发光
ctx.restore();
drawEnemy();      // 敌机不受影响 ✓

save() 保存的状态包括:

  • 变换矩阵(translate, rotate, scale)
  • 裁剪区域(clip)
  • 样式属性(fillStyle, strokeStyle, shadowBlur 等)
  • 字体、文本对齐等

5.2 渐变(Gradient)

Canvas 支持两种渐变:

javascript 复制代码
// 线性渐变:createLinearGradient(x1, y1, x2, y2)
const grad = ctx.createLinearGradient(0, 0, 0, 100);
grad.addColorStop(0, '#ffffff');    // 起点颜色
grad.addColorStop(0.5, '#ffee00');  // 中间颜色
grad.addColorStop(1, '#ff8800');    // 终点颜色
ctx.fillStyle = grad;

// 径向渐变:createRadialGradient(x1, y1, r1, x2, y2, r2)
const radialGrad = ctx.createRadialGradient(50, 50, 0, 50, 50, 50);
radialGrad.addColorStop(0, '#ffffff');
radialGrad.addColorStop(1, '#000000');

5.3 性能优化建议

优化项 说明
减少状态切换 批量绘制相同颜色的图形,减少 fillStyle 的修改次数
离屏 Canvas 复杂且不常变的背景可以预渲染到离屏 Canvas,然后一次性 drawImage
限制粒子数量 粒子系统要设置上限,防止爆炸特效过多导致卡顿
碰撞检测优化 大量对象时考虑空间分割(如四叉树),而不是 O(n²) 暴力遍历
避免浮点坐标 非整数坐标会导致抗锯齿,影响性能,必要时使用 Math.floor

六、项目运行与调试

环境要求

  • Node.js ≥ 16
  • 现代浏览器(Chrome / Edge / Firefox)

启动步骤

bash 复制代码
# 1. 进入项目目录
cd airplane

# 2. 安装依赖
npm install

# 3. 启动开发服务器(默认端口 3000,自动打开浏览器)
npm run dev

游戏操作

按键 功能
↑ ↓ ← →W S A D 移动飞机
空格键 发射子弹
R 游戏结束后重新开始

七、总结与进阶方向

📊 学习路径回顾

less 复制代码
基础绘图(1.html)
  │  掌握: Canvas元素、渲染上下文、fillRect/strokeRect/clearRect
  │  理解了: Canvas坐标系、即时模式渲染
  ↓
动画原理(2.html)
  │  掌握: requestAnimationFrame、动画循环、帧更新模式
  │  理解了: 清除-更新-绘制 三步循环
  ↓
实战游戏(airplane/)
  │  掌握: 多对象管理、碰撞检测、粒子系统、状态机、难度曲线
  │  理解了: 完整的游戏架构设计
  ↓
🎉 恭喜!你已经具备了 Canvas 开发的完整知识体系

🚀 进阶方向

  1. WebGL / WebGPU:当 2D Canvas 性能不够时,可以进阶到 WebGL 进行 GPU 加速的 3D 渲染
  2. 游戏引擎:了解 Phaser.js、PixiJS 等基于 Canvas/WebGL 的专业游戏框架
  3. 数据可视化:结合 ECharts 图表库,Canvas 在数据可视化领域有广泛应用
  4. 图像处理 :使用 getImageData / putImageData 实现图像滤镜、美颜等效果
  5. 更多游戏特性:音效(Web Audio API)、关卡系统、道具系统、触屏支持

如果你跟着代码一步步实践下来,相信你已经从 Canvas 新手成长为能够独立开发小游戏的开发者了。Canvas 的世界远不止于此,持续探索,你会发现更多有趣的可能性!


相关推荐
master3362 小时前
SSL 证书链问题导致微信小程序无法正常工作
网络协议·微信小程序·ssl
wuxia21181 天前
在5种环境中编写点击元素改变内容和颜色的JavaScript程序
javascript·微信小程序·vue·jquery·react
it-10241 天前
抖音快手短视频去水印微信小程序/一键去水印/小程序去水印接口代码
微信小程序·小程序·php
夏天测2 天前
微信小程序自动化漏洞挖掘流水线:从缓存提取到密钥验证全流程实战
python·网络安全·微信小程序·漏洞挖掘
it-10242 天前
微信小程序短视频去水印/抖音短视频去水印/免费去水印源码
微信小程序·小程序·视频去水印
kidding7233 天前
高效备忘清单工具类小程序
前端·计算机网络·微信小程序·小程序
前端 贾公子3 天前
小程序蓝牙打印探索与实践 (最终章)
前端·微信小程序·小程序
小羊Yveesss3 天前
2026年个人能做微信小程序吗?
微信小程序·小程序
kidding7233 天前
BMI 健康测量仪工具类小程序
前端·微信小程序·小程序