🚀 手把手用 HTML5 Canvas 从零打造飞机大战游戏,代码全开源!

🚀 手把手用 HTML5 Canvas 从零打造飞机大战游戏,代码全开源!

摘要:本文从 Canvas 基础 API 讲起,手把手带你用纯 JavaScript + Canvas 2D 打造一个完整的飞机大战游戏,涵盖视差滚动、粒子特效、碰撞检测、难度递增等核心机制。


📌 前言

上周我让 AI 帮我写个飞机大战,它给了我一个 30 行的"demo"------飞机是一个蓝色方块,子弹是一条直线......我决定自己用 Canvas 从零撸一个真正能玩的版本。

本文记录了整个开发过程,从 Canvas 基础 API 到完整的飞机大战游戏,包含视差星空、粒子特效、波次难度递增等机制。所有代码已开源,文末附项目地址。

🎨 Canvas 2D 不只是画矩形,它能做游戏、做数据可视化、做一切你能想象到的酷炫页面效果。


🎯 本文适合谁

  • 想入门 Canvas 2D 绘图的前端开发者
  • 对游戏开发感兴趣但不知从何下手的同学
  • 想了解 requestAnimationFrame、碰撞检测、粒子系统等游戏开发核心概念的读者

🎮 效果预览

💡 快速体验

bash 复制代码
git clone https://gitee.com/zhou-wenqiang-c/ai_doubao_zwq.git
cd ai_doubao_zwq/fe/html5/canvas/airplan
npm install
npm run dev

浏览器打开后即可体验完整游戏效果。


一、Canvas 是什么?

HTML5 的 <canvas> 标签提供了一块画布,配合 JavaScript 的 Canvas API,你可以在上面自由绘制任何图形------从简单的矩形、圆形,到复杂的游戏角色和粒子特效。

html 复制代码
<canvas id="myCanvas" width="600" height="400" style="border: 1px solid #333;">
    你的浏览器不支持 Canvas
</canvas>

⚠️ <canvas> 标签内部的文字是降级内容,只有在浏览器不支持 Canvas 时才会显示。现代浏览器基本都支持,但留着它是个好习惯。

核心三步走

js 复制代码
// 1. 获取 canvas 元素
const canvas = document.querySelector('#myCanvas');

// 2. 获取绘图上下文(2d 或 webgl)
const ctx = canvas.getContext('2d');

// 3. 开始绘制
ctx.fillStyle = '#4299ee';
ctx.fillRect(20, 20, 100, 80); // 矩形

getContext('2d') 是关键 ------它返回一个 CanvasRenderingContext2D 对象,所有绘制操作都通过它完成。如果要做 3D,则使用 getContext('webgl'),配合 Three.js 等库可以实现 WebGL 3D 渲染。


二、Canvas 2D 基础 API

2.1 矩形绘制

Canvas 提供了三种矩形方法:

js 复制代码
// 填充矩形
ctx.fillStyle = '#4299ee';
ctx.fillRect(20, 20, 100, 80);

// 描边矩形
ctx.strokeStyle = '#f56565';
ctx.lineWidth = 4;
ctx.strokeRect(150, 20, 100, 80);

// 清除矩形区域(擦除)
ctx.clearRect(50, 50, 40, 30);

参数说明:fillRect(x, y, width, height)

  • x, y:矩形左上角坐标(Canvas 坐标系原点在左上角)
  • width, height:宽和高

2.2 颜色系统

js 复制代码
ctx.fillStyle = '#4299ee';      // 填充色
ctx.strokeStyle = '#f56565';    // 描边色

颜色值支持:

  • 十六进制:#ff0000
  • RGB/RGBA:rgb(255,0,0) / rgba(255,0,0,0.5)
  • 颜色关键字:red, blue, transparent

2.3 路径绘制(画任意图形)

矩形只是开胃菜,Canvas 真正强大的地方在于路径绘制

js 复制代码
ctx.beginPath();
ctx.moveTo(100, 50);    // 移动到起点
ctx.lineTo(200, 50);    // 画直线到
ctx.lineTo(150, 150);
ctx.closePath();         // 闭合路径
ctx.fillStyle = '#ff6600';
ctx.fill();

绘制圆形/弧线:

js 复制代码
ctx.beginPath();
ctx.arc(x, y, radius, startAngle, endAngle);
ctx.fill();

📝 arc() 的角度用弧度制Math.PI * 2 就是一整圈。

2.4 渐变

js 复制代码
// 线性渐变
const grad = ctx.createLinearGradient(x1, y1, x2, y2);
grad.addColorStop(0, '#a0e0ff');
grad.addColorStop(1, '#2080ff');
ctx.fillStyle = grad;

// 径向渐变
const radGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
radGrad.addColorStop(0, '#ffff00');
radGrad.addColorStop(1, '#ff0066');
ctx.fillStyle = radGrad;

2.5 文字绘制

js 复制代码
ctx.font = 'bold 36px monospace';
ctx.textAlign = 'center';
ctx.fillStyle = '#4a9eff';
ctx.fillText('雷霆战机', canvas.width / 2, 100);

三、动画的灵魂:requestAnimationFrame

为什么不能用 setInterval?

很多初学者会这样写动画:

js 复制代码
// ❌ 错误示范
setInterval(() => {
    // 更新 + 绘制
}, 16); // 大约 60fps

问题在于:

  1. setInterval 的时间精度不稳定,可能和显示器刷新率不同步
  2. 后台标签页仍然在执行,浪费性能
  3. 无法自动适配高刷屏幕(120Hz、144Hz)

正确姿势:requestAnimationFrame

js 复制代码
function animate() {
    // 清除画布
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 更新位置
    x += speed;

    // 绘制
    ctx.fillRect(x, y, width, height);

    // 递归调用
    requestAnimationFrame(animate);
}

animate(); // 启动动画

requestAnimationFrame 的优势:

  • 浏览器自动匹配屏幕刷新率(通常 60fps,高刷屏更高)
  • 后台标签页自动暂停,节省资源
  • 由浏览器统一调度,避免掉帧

完整的移动矩形示例

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

let x = 20;
const y = 20;
const width = 100;
const height = 80;
const speed = 3;

function animate() {
    // 1. 擦掉之前的画面
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 2. 画上新的位置
    ctx.fillStyle = '#4299ee';
    ctx.fillRect(x, y, width, height);

    // 3. 更新坐标
    x += speed;

    // 4. 超出边界后从左边重新出现
    if (x > canvas.width) {
        x = -width;
    }

    // 5. 请求下一帧
    requestAnimationFrame(animate);
}

animate();

💡 动画三要素:清除 → 更新 → 绘制。这就是所有 Canvas 动画的核心循环。


四、实战:用 Canvas 做一个飞机大战

理论讲完了,来点硬核的------用纯 Canvas 2D 实现一个完整的飞机大战游戏。

4.1 项目架构

bash 复制代码
airplan/
├── src/
│   ├── main.js          # 入口
│   └── game/
│       ├── Game.js       # 游戏主控制器
│       ├── Player.js     # 玩家飞机
│       ├── Enemy.js      # 敌机系统
│       ├── Bullet.js     # 子弹对象池
│       ├── Background.js # 星空背景
│       ├── Collision.js  # 碰撞检测
│       ├── Explosion.js  # 爆炸特效
│       ├── PowerUp.js    # 道具系统
│       ├── HUD.js        # UI 显示
│       └── utils.js      # 工具函数
├── index.html
└── package.json

4.2 游戏主循环

所有游戏的核心都是一个主循环,每一帧做三件事:更新逻辑、绘制画面、请求下一帧。

js 复制代码
export class Game {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.state = 'menu'; // menu | playing | paused | gameover
    this.lastTime = 0;
    this.running = true;
  }

  start() {
    this.lastTime = performance.now();
    this._loop(this.lastTime);
  }

  _loop(currentTime) {
    if (!this.running) return;

    // 计算 deltaTime(两帧之间的时间差)
    const deltaTime = currentTime - this.lastTime;
    this.lastTime = currentTime;

    this._update(deltaTime);  // 更新逻辑
    this._draw();             // 绘制画面

    requestAnimationFrame((t) => this._loop(t));
  }
}

为什么需要 deltaTime?

不同设备帧率不同,如果直接 x += 3,在 30fps 和 60fps 下移动速度会不一样。用 deltaTime 做时间归一化,确保所有设备上体验一致:

js 复制代码
_update(deltaTime) {
  // 将速度转换为「每毫秒移动的像素数」,乘以 deltaTime 后
  // 无论帧率高低,每秒移动的像素总量相同
  const speed = 0.3; // 每毫秒 0.3 像素 → 约 18px/帧@60fps
  this.player.x += speed * deltaTime;

  // 同理,射击间隔、粒子生命周期等都基于 deltaTime
  this.shootTimer += deltaTime;
}

4.3 星空背景与视差滚动

游戏背景不是一张静态图,而是三层星星以不同速度滚动,产生视差效果(近处的快、远处的慢):

js 复制代码
class Star {
  constructor(canvasWidth, canvasHeight) {
    this.x = randomInt(0, canvasWidth);
    this.y = randomInt(0, canvasHeight);
    this.size = randomFloat(0.5, 2.5);
    this.speed = this.size * 0.8; // 大星星更快 → 看起来更近
    this.alpha = randomFloat(0.3, 1.0);
  }

  update() {
    this.y += this.speed;
    if (this.y > this.canvasHeight) {
      this.x = randomInt(0, this.canvasWidth);
      this.y = randomInt(-20, -5);
    }
  }

  draw(ctx) {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
    ctx.fillStyle = `rgba(255, 255, 255, ${this.alpha})`;
    ctx.fill();
  }
}

export class Background {
  constructor(canvasWidth, canvasHeight) {
    // 三层星星:远景小而慢,近景大而快
    this.layers = [
      this._createStars(40, true),  // 远景
      this._createStars(25, true),  // 中景
      this._createStars(15, true),  // 近景
    ];
  }

  draw(ctx) {
    // 深色背景
    ctx.fillStyle = '#0a0a1a';
    ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);

    // 逐层绘制
    for (const layer of this.layers) {
      for (const star of layer) {
        star.draw(ctx);
      }
    }
  }
}

4.4 玩家飞机绘制

用纯代码画一架飞机,不需要任何图片资源:

js 复制代码
draw(ctx) {
  const cx = this.x + this.width / 2;

  // 机身(多边形)
  ctx.fillStyle = '#4a9eff';
  ctx.beginPath();
  ctx.moveTo(cx, this.y);              // 机头
  ctx.lineTo(cx + 8, this.y + 15);
  ctx.lineTo(cx + 18, this.y + 45);
  ctx.lineTo(cx + 12, this.y + this.height);
  ctx.lineTo(cx - 12, this.y + this.height);
  ctx.lineTo(cx - 18, this.y + 45);
  ctx.lineTo(cx - 8, this.y + 15);
  ctx.closePath();
  ctx.fill();

  // 驾驶舱(渐变椭圆)
  const cockpitGrad = ctx.createLinearGradient(cx, this.y + 12, cx, this.y + 30);
  cockpitGrad.addColorStop(0, '#a0e0ff');
  cockpitGrad.addColorStop(1, '#2080ff');
  ctx.fillStyle = cockpitGrad;
  ctx.beginPath();
  ctx.ellipse(cx, this.y + 22, 5, 8, 0, 0, Math.PI * 2);
  ctx.fill();

  // 机翼
  ctx.fillStyle = '#3070d0';
  ctx.beginPath();
  ctx.moveTo(cx - 10, this.y + 30);
  ctx.lineTo(cx - 28, this.y + 48);
  ctx.lineTo(cx - 25, this.y + 52);
  ctx.lineTo(cx - 12, this.y + 42);
  ctx.closePath();
  ctx.fill();
}

🎨 看到了吗?一架飞机就是几个 moveTo + lineTo 组合出来的多边形。Canvas 绘制的本质就是用代码画画

4.5 敌机系统与难度递增

敌机不是随机出现的,而是有波次系统难度曲线

js 复制代码
const ENEMY_TYPES = {
  small:  { width: 36, height: 36, hp: 1, speed: 2,   score: 100 },
  medium: { width: 50, height: 50, hp: 3, speed: 1.5, score: 300 },
  large:  { width: 70, height: 70, hp: 8, speed: 1,   score: 800 },
};

// 难度递增:每 10 秒提高一波
if (this.difficultyTimer > 10000) {
  this.difficultyTimer = 0;
  this.spawnInterval = Math.max(500, this.spawnInterval - 100);
  this.waveCount++;
}

// 根据波次调整敌机类型概率
_spawnWave() {
  const roll = Math.random();
  let type;

  if (this.waveCount < 3) {
    // 前期:小飞机为主
    type = roll < 0.7 ? 'small' : roll < 0.95 ? 'medium' : 'large';
  } else if (this.waveCount < 6) {
    // 中期:中型飞机增多
    type = roll < 0.5 ? 'small' : roll < 0.85 ? 'medium' : 'large';
  } else {
    // 后期:大飞机频繁出现
    type = roll < 0.3 ? 'small' : roll < 0.7 ? 'medium' : 'large';
  }
}

不同敌机还有不同的行为模式:

  • 小型敌机:左右漂移(正弦波运动)
  • 中型敌机:追踪玩家 X 轴位置
  • 大型敌机:Boss 级,会发射子弹

4.6 碰撞检测

游戏碰撞检测的核心是矩形相交判断(AABB 碰撞检测):

js 复制代码
function checkCollision(a, b) {
  return (
    a.x < b.x + b.width &&
    a.x + a.width > b.x &&
    a.y < b.y + b.height &&
    a.y + a.height > b.y
  );
}

💡 这是最简单高效的碰撞检测算法。对于圆形物体,可以用圆心距判断:distance < r1 + r2

4.7 粒子特效

飞机尾焰和爆炸效果都是用粒子系统实现的:

js 复制代码
// 尾迹粒子
_updateTrail() {
  // 获取飞机位置
  const cx = this.player.x + this.player.width / 2;
  const bottom = this.player.y + this.player.height;

  // 随机生成新粒子
  if (Math.random() < 0.3) {
    this.trailParticles.push({
      x: cx + (Math.random() - 0.5) * 8,
      y: bottom + Math.random() * 5,
      vx: (Math.random() - 0.5) * 0.5,
      vy: 0.5 + Math.random(),
      life: 1.0,              // 生命周期
      size: 1 + Math.random() * 2,
    });
  }

  // 更新所有粒子
  for (let i = this.trailParticles.length - 1; i >= 0; i--) {
    const p = this.trailParticles[i];
    p.x += p.vx;
    p.y += p.vy;
    p.life -= 0.03;           // 逐渐消亡
    p.size *= 0.98;           // 逐渐缩小
    if (p.life <= 0) this.trailParticles.splice(i, 1);
  }
}

// 绘制粒子
for (const p of this.trailParticles) {
  ctx.globalAlpha = p.life * 0.5;  // 透明度随生命周期变化
  ctx.fillStyle = '#ff8800';
  ctx.beginPath();
  ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
  ctx.fill();
}

4.8 交互控制

支持触摸、鼠标、键盘三种输入方式:

js 复制代码
// 触摸控制(移动端)
canvas.addEventListener('touchmove', (e) => {
  e.preventDefault();
  const touch = e.touches[0];
  const rect = canvas.getBoundingClientRect();
  player.handleTouchMove(touch.clientX - rect.left, touch.clientY - rect.top);
}, { passive: false });

// 键盘控制(PC 端)
window.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowLeft' || e.key === 'a')
    player.x = Math.max(0, player.x - speed);
  if (e.key === 'ArrowRight' || e.key === 'd')
    player.x = Math.min(canvas.width - player.width, player.x + speed);
  if (e.key === 'ArrowUp' || e.key === 'w')
    player.y = Math.max(0, player.y - speed);
  if (e.key === 'ArrowDown' || e.key === 's')
    player.y = Math.min(canvas.height - player.height, player.y + speed);
});

⚠️ 注意 passive: falsee.preventDefault() 的配合------在移动端,如果不阻止默认的触摸行为,页面会跟着滚动。


五、Canvas 数据可视化

除了游戏,Canvas 最常见的应用场景是数据可视化。虽然可以用原生 Canvas API 手动绘制图表,但实际开发中更推荐使用成熟库:

html 复制代码
<div id="chart" style="width: 100%; height: 600px;"></div>
<script type="module" src="/src/main.js"></script>
js 复制代码
// ECharts 示例
import * as echarts from 'echarts';

const chart = echarts.init(document.getElementById('chart'));

const option = {
  title: { text: '月度销售数据' },
  xAxis: {
    type: 'category',
    data: ['1月', '2月', '3月', '4月', '5月', '6月',
           '7月', '8月', '9月', '10月', '11月', '12月']
  },
  yAxis: { type: 'value' },
  series: [{
    type: 'bar',
    data: [320, 280, 350, 410, 480, 520, 460, 530, 490, 560, 680, 750]
  }]
};

chart.setOption(option);

ECharts 底层就是用 Canvas(或 SVG)渲染的。理解 Canvas 原理,能帮你更好地理解这些库的工作方式,也方便做自定义渲染和性能优化。


六、总结与实践建议

Canvas 能做什么?

场景 示例
2D 游戏 飞机大战、贪吃蛇、俄罗斯方块
数据可视化 图表、仪表盘、热力图
图片处理 滤镜、裁剪、水印
动画特效 粒子系统、Loading 动画
3D 渲染 配合 WebGL / Three.js

学习路线

  1. 基础:矩形、路径、颜色、文字
  2. 进阶:动画循环、requestAnimationFrame、deltaTime
  3. 实战:碰撞检测、粒子系统、对象池
  4. 高阶:WebGL、Three.js、GPU 加速

性能优化小贴士

  • 对象池:子弹、粒子等频繁创建销毁的对象,用对象池复用
  • 离屏 Canvas:静态内容先画到离屏 Canvas,再整体贴到主画布
  • requestAnimationFrame:永远不要用 setInterval 做动画
  • 减少状态切换:批量绘制相同颜色/样式的图形

💬 交流讨论

你用 Canvas 做过什么有趣的项目?欢迎在评论区分享!

如果这篇文章对你有帮助,点赞 👍 + 收藏 ⭐ + 关注 👆,后续会更新更多 Canvas 实战内容:

  • 粒子特效进阶:火焰、烟雾、魔法效果
  • WebGL 入门:从 2D 到 3D 的跨越
  • 性能优化:让你的游戏跑满 120fps

📦 项目地址

完整项目代码已开源,欢迎 Star ⭐

🔗 gitee.com/zhou-wenqia...


🏷️ 推荐标签

Canvas 前端 JavaScript 游戏开发 HTML5 2D动画


🚀 Canvas 的魅力在于------你写的每一行代码,都在屏幕上留下像素。这种从零创造的掌控感,是任何 UI 框架都无法替代的。


觉得有用?点个赞👍收藏⭐关注👆,下次更新更多 Canvas 玩法!

相关推荐
奋斗吧程序媛2 小时前
补充一个小知识点:有关@click.native
前端·vue.js
DJ斯特拉2 小时前
axios快速使用
开发语言·前端·javascript
智通2 小时前
可取消的异步任务与 AbortController
javascript
还有多久拿退休金2 小时前
Ant Design Tree 搜索定位避坑指南:虚拟滚动下如何实现高亮与精准定位
前端·react.js
小月土星2 小时前
CSS 3D 从入门到炫技:手把手教你写一个旋转立方体
前端·css
Hilaku3 小时前
AI 写代码越快,为什么 Code Review 越不能省?
前端·javascript·程序员
sugar__salt3 小时前
从网页小游戏到数据可视化:掌握 HTML5 Canvas 核心能力
前端·信息可视化·html5
北极星日淘4 小时前
前端 i18n 中日双语交互 + 翻译客服接口联动方案|日系海淘平台中文友好化开发实战
前端·交互
現実逃避と4 小时前
WIN10 Edge连续关闭多个标签页导致资源管理器崩溃临时解决办法
前端·edge