当前位置: 首页 > news >正文

用原生JS和Canvas复刻Flappy Bird:从零实现一个能玩的网页小游戏

用原生JS和Canvas复刻Flappy Bird:从零实现一个能玩的网页小游戏

在游戏开发的世界里,没有什么比亲手实现一个经典游戏更能检验和提升编程技能了。Flappy Bird这个看似简单的游戏,实际上包含了游戏开发中最核心的几个概念:游戏循环、物理模拟、碰撞检测和用户交互。对于前端开发者来说,用原生JavaScript和Canvas来复刻它,不仅能巩固基础,还能深入理解游戏开发的底层原理。

与使用现成游戏引擎不同,原生实现让我们有机会从零开始构建每一个游戏组件,理解每一行代码背后的意义。本文将带你一步步实现一个完整的Flappy Bird游戏,重点不是复制代码,而是理解"为什么这么做"——为什么选择Canvas而不是CSS动画?为什么游戏循环要这样设计?不同的碰撞检测方法各有什么优劣?

1. 游戏基础架构搭建

1.1 Canvas画布初始化

任何Canvas游戏的第一步都是创建和配置画布。我们不仅需要设置画布尺寸,还要考虑如何组织代码结构以便后续扩展:

const Game = { canvas: document.createElement('canvas'), ctx: null, width: 360, height: 640, init() { this.canvas.width = this.width; this.canvas.height = this.height; this.ctx = this.canvas.getContext('2d'); document.body.appendChild(this.canvas); // 游戏状态管理 this.state = { current: 'ready', // ready, playing, gameover score: 0 }; // 初始化游戏对象 this.bird = new Bird(this); this.pipes = new Pipes(this); // 开始游戏循环 this.lastTime = 0; requestAnimationFrame(this.loop.bind(this)); }, loop(timestamp) { const deltaTime = timestamp - this.lastTime; this.lastTime = timestamp; this.update(deltaTime); this.render(); requestAnimationFrame(this.loop.bind(this)); }, update(deltaTime) { // 根据游戏状态更新不同对象 if (this.state.current === 'playing') { this.bird.update(deltaTime); this.pipes.update(deltaTime); } }, render() { // 清空画布 this.ctx.clearRect(0, 0, this.width, this.height); // 绘制背景 this.ctx.fillStyle = '#70c5ce'; this.ctx.fillRect(0, 0, this.width, this.height); // 根据游戏状态渲染不同内容 this.pipes.render(this.ctx); this.bird.render(this.ctx); // 显示分数 this.ctx.fillStyle = '#fff'; this.ctx.font = '30px Arial'; this.ctx.fillText(this.state.score, 20, 40); } }; // 启动游戏 window.onload = () => Game.init();

这个基础架构有几个关键设计点:

  1. 使用requestAnimationFrame:相比setInterval,它能提供更流畅的动画效果,并自动匹配显示器的刷新率
  2. deltaTime计算:记录帧间隔时间,确保在不同刷新率设备上游戏速度一致
  3. 状态管理:通过state对象管理游戏的不同阶段(准备、进行中、结束)

1.2 游戏对象抽象

良好的面向对象设计能让代码更易维护和扩展。我们为游戏中的主要元素创建类:

class GameObject { constructor(game) { this.game = game; this.x = 0; this.y = 0; this.width = 0; this.height = 0; } update(deltaTime) { // 由子类实现具体逻辑 } render(ctx) { // 由子类实现具体绘制 } get bounds() { return { left: this.x, right: this.x + this.width, top: this.y, bottom: this.y + this.height }; } }

这个基类定义了游戏对象的基本属性和方法,后续的Bird和Pipes类都将继承它。这种设计模式的优势在于:

  • 代码复用:公共方法和属性只需定义一次
  • 统一接口:所有游戏对象都有update和render方法,便于管理
  • 类型检查:可以通过instanceof判断对象类型

2. 游戏核心机制实现

2.1 小鸟物理系统

Flappy Bird的核心玩法在于控制小鸟飞行,这需要模拟重力和跳跃物理效果:

class Bird extends GameObject { constructor(game) { super(game); this.width = 34; this.height = 24; this.x = 60; this.y = game.height / 2 - this.height / 2; // 物理参数 this.velocity = 0; this.gravity = 0.5; this.jumpForce = -10; this.rotation = 0; // 控制标志 this.isFlapping = false; // 加载图像资源 this.image = new Image(); this.image.src = 'bird.png'; } update(deltaTime) { // 应用重力 this.velocity += this.gravity; this.y += this.velocity; // 旋转效果 this.rotation = Math.min(Math.max(this.velocity * 5, -25), 90); // 边界检测 if (this.y < 0) { this.y = 0; this.velocity = 0; } if (this.y > this.game.height - this.height) { this.y = this.game.height - this.height; this.game.state.current = 'gameover'; } } jump() { this.velocity = this.jumpForce; this.isFlapping = true; setTimeout(() => this.isFlapping = false, 100); } render(ctx) { ctx.save(); ctx.translate(this.x + this.width / 2, this.y + this.height / 2); ctx.rotate(this.rotation * Math.PI / 180); // 绘制小鸟 ctx.drawImage( this.image, -this.width / 2, -this.height / 2, this.width, this.height ); ctx.restore(); } }

物理模拟的关键参数:

参数初始值作用
velocity0当前垂直速度(正数向下)
gravity0.5重力加速度(每帧增加的速度)
jumpForce-10点击时施加的向上力
rotation0根据速度计算的旋转角度

2.2 管道系统实现

管道是游戏的主要障碍物,需要随机生成并移动:

class Pipes extends GameObject { constructor(game) { super(game); this.pipes = []; this.gap = 120; // 上下管道间的空隙 this.frequency = 1500; // 生成新管道的间隔(ms) this.speed = 2; // 管道移动速度 this.lastPipeTime = 0; this.width = 52; } update(deltaTime) { const now = Date.now(); // 生成新管道 if (now - this.lastPipeTime > this.frequency) { this.createPipe(); this.lastPipeTime = now; } // 更新所有管道位置 for (let i = this.pipes.length - 1; i >= 0; i--) { this.pipes[i].x -= this.speed; // 移除屏幕外的管道 if (this.pipes[i].x < -this.width) { this.pipes.splice(i, 1); this.game.state.score++; // 通过一对管道得1分 } } } createPipe() { const height = Math.random() * 200 + 100; // 随机高度 const topPipe = { x: this.game.width, y: 0, height }; const bottomPipe = { x: this.game.width, y: height + this.gap, height: this.game.height - height - this.gap }; this.pipes.push(topPipe, bottomPipe); } render(ctx) { ctx.fillStyle = '#74bf2e'; this.pipes.forEach(pipe => { ctx.fillRect(pipe.x, pipe.y, this.width, pipe.height); // 管道顶部/底部的装饰 ctx.fillStyle = '#5da22d'; ctx.fillRect(pipe.x - 3, pipe.y, this.width + 6, 20); }); } }

管道系统的关键设计:

  1. 随机生成:通过Math.random()创建不同高度的管道
  2. 对象池模式:复用移出屏幕的管道对象,避免频繁创建销毁
  3. 碰撞体积:虽然绘制了装饰部分,但碰撞检测只考虑主体矩形

3. 碰撞检测与交互

3.1 精确碰撞检测

游戏需要检测小鸟与管道、地面的碰撞:

class Game { // ...其他代码... checkCollisions() { // 地面碰撞 if (this.bird.y + this.bird.height >= this.height) { this.state.current = 'gameover'; return; } // 管道碰撞 for (const pipe of this.pipes.pipes) { if ( this.bird.x < pipe.x + this.pipes.width && this.bird.x + this.bird.width > pipe.x && this.bird.y < pipe.y + pipe.height && this.bird.y + this.bird.height > pipe.y ) { this.state.current = 'gameover'; return; } } } update(deltaTime) { if (this.state.current === 'playing') { this.bird.update(deltaTime); this.pipes.update(deltaTime); this.checkCollisions(); } } }

碰撞检测的几种实现方式对比:

方法精度性能适用场景
矩形检测简单几何形状
圆形检测圆形或近似圆形物体
像素检测需要精确碰撞
分离轴定理复杂多边形

对于Flappy Bird这种简单游戏,矩形检测完全够用。如果需要更精确的检测,可以考虑:

  1. 使用多个小矩形组合复杂形状
  2. 为小鸟实现圆形检测(更符合其形状)
  3. 预先计算碰撞遮罩

3.2 用户输入处理

游戏通过点击或按键控制小鸟跳跃:

class Game { // ...其他代码... init() { // ...其他初始化... this.setupControls(); } setupControls() { // 鼠标/触摸控制 this.canvas.addEventListener('click', () => { if (this.state.current === 'ready') { this.state.current = 'playing'; } this.bird.jump(); }); // 键盘控制 document.addEventListener('keydown', (e) => { if (e.code === 'Space') { if (this.state.current === 'ready') { this.state.current = 'playing'; } this.bird.jump(); } }); // 移动端触摸控制 this.canvas.addEventListener('touchstart', (e) => { e.preventDefault(); if (this.state.current === 'ready') { this.state.current = 'playing'; } this.bird.jump(); }); } }

输入处理的最佳实践:

  • 多平台支持:同时考虑鼠标、键盘和触摸输入
  • 事件委托:在canvas上监听事件而非整个文档
  • 防误触:移动端使用touchstart而非click减少延迟
  • 状态检查:根据游戏状态决定输入效果

4. 游戏优化与扩展

4.1 性能优化技巧

即使是这样的小游戏,优化也很重要:

// 图像资源预加载 const assets = { bird: 'bird.png', background: 'bg.png', pipe: 'pipe.png' }; const loadedAssets = {}; let assetsLoaded = 0; function loadAssets() { Object.keys(assets).forEach(key => { const img = new Image(); img.src = assets[key]; img.onload = () => { loadedAssets[key] = img; assetsLoaded++; if (assetsLoaded === Object.keys(assets).length) { Game.init(); } }; }); } // 使用离屏canvas缓存静态元素 const backgroundCache = document.createElement('canvas'); backgroundCache.width = Game.width; backgroundCache.height = Game.height; const bgCtx = backgroundCache.getContext('2d'); // 绘制背景到缓存 bgCtx.fillStyle = '#70c5ce'; bgCtx.fillRect(0, 0, Game.width, Game.height);

优化策略对比:

优化方法实现难度效果适用场景
资源预加载有图像/音频资源时
对象池频繁创建销毁对象
离屏缓存静态或重复绘制内容
脏矩形极高局部更新的复杂场景

4.2 游戏状态与特效

增强游戏体验的视觉效果:

class Game { // ...其他代码... render() { // 绘制缓存的背景 this.ctx.drawImage(backgroundCache, 0, 0); // 游戏状态相关渲染 switch (this.state.current) { case 'ready': this.renderReadyScreen(); break; case 'gameover': this.renderGameOver(); break; } // 绘制游戏对象 this.pipes.render(this.ctx); this.bird.render(this.ctx); this.renderScore(); } renderReadyScreen() { this.ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; this.ctx.fillRect(0, 0, this.width, this.height); this.ctx.fillStyle = '#fff'; this.ctx.font = '30px Arial'; this.ctx.textAlign = 'center'; this.ctx.fillText('点击屏幕开始游戏', this.width / 2, this.height / 2); this.ctx.textAlign = 'left'; } renderGameOver() { this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; this.ctx.fillRect(0, 0, this.width, this.height); this.ctx.fillStyle = '#fff'; this.ctx.font = '30px Arial'; this.ctx.textAlign = 'center'; this.ctx.fillText('游戏结束', this.width / 2, this.height / 2 - 40); this.ctx.fillText(`得分: ${this.state.score}`, this.width / 2, this.height / 2); this.ctx.fillText('点击重新开始', this.width / 2, this.height / 2 + 40); this.ctx.textAlign = 'left'; } renderScore() { this.ctx.fillStyle = '#fff'; this.ctx.font = '30px Arial'; this.ctx.fillText(this.state.score, 20, 40); } }

可以进一步添加的特效:

  1. 粒子效果:小鸟撞击时的爆炸粒子
  2. 视差滚动:多层背景营造深度感
  3. 动画过渡:游戏状态切换时的平滑过渡
  4. 音效反馈:跳跃、得分和碰撞的音效

5. 项目结构与构建

5.1 模块化组织代码

随着功能增加,需要更好的代码组织方式:

/flappy-bird ├── index.html ├── assets/ │ ├── images/ │ ├── sounds/ ├── src/ │ ├── game.js # 主游戏类 │ ├── bird.js # 小鸟类 │ ├── pipes.js # 管道系统 │ ├── utils.js # 工具函数 │ └── main.js # 入口文件 └── style.css

使用ES6模块拆分代码:

// game.js export default class Game { // ...游戏主逻辑... } // bird.js export default class Bird extends GameObject { // ...小鸟实现... } // main.js import Game from './game.js'; import Bird from './bird.js'; import Pipes from './pipes.js'; const game = new Game(); game.init();

模块化的优势:

  • 关注点分离:每个类/模块职责单一
  • 可维护性:更容易定位和修改特定功能
  • 可测试性:可以单独测试各个模块
  • 团队协作:不同开发者可以并行工作

5.2 构建与部署

现代前端项目通常需要构建步骤:

  1. 安装必要工具

    npm init -y npm install --save-dev webpack webpack-cli babel-loader @babel/core @babel/preset-env
  2. webpack配置

    // webpack.config.js module.exports = { entry: './src/main.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] } };
  3. 添加构建脚本

    { "scripts": { "build": "webpack --mode production", "dev": "webpack --mode development --watch" } }
  4. HTML引入

    <!DOCTYPE html> <html> <head> <title>Flappy Bird</title> <link rel="stylesheet" href="style.css"> </head> <body> <script src="dist/bundle.js"></script> </body> </html>

构建流程带来的好处:

  • 代码压缩:减少文件体积
  • 浏览器兼容:通过Babel转译ES6+语法
  • 资源优化:可以集成图片压缩等插件
  • 开发体验:支持热更新等功能

6. 进阶方向与扩展思路

6.1 添加更多游戏功能

基础版本完成后,可以考虑扩展:

  1. 难度系统

    • 随分数增加管道移动速度
    • 缩小管道间隙
    • 增加特殊障碍物
  2. 道具系统

    • 临时无敌
    • 分数加倍
    • 磁铁吸引金币
  3. 成就系统

    • 连续通过多个管道的连击奖励
    • 特定分数里程碑
    • 特殊动作成就
  4. 多人模式

    • 本地双人轮流游戏
    • 网络对战看谁坚持更久
    • 异步比分排行榜

6.2 跨平台适配

让游戏适应不同平台:

  1. 响应式设计

    function resize() { const ratio = window.innerHeight / Game.height; Game.canvas.style.width = `${Game.width * ratio}px`; Game.canvas.style.height = `${window.innerHeight}px`; } window.addEventListener('resize', resize); resize();
  2. 移动端优化

    • 调整控制灵敏度
    • 添加虚拟按钮
    • 优化触控反馈
  3. PWA支持

    • 添加manifest文件
    • 实现Service Worker缓存
    • 支持离线游玩

6.3 性能监控与调试

开发过程中的性能工具:

  1. Chrome DevTools

    • Performance面板分析帧率
    • Memory面板检查内存泄漏
    • Layers查看复合层情况
  2. Stats.js

    import Stats from 'stats.js'; const stats = new Stats(); stats.showPanel(0); // 0: fps, 1: ms, 2: mb document.body.appendChild(stats.dom); function loop() { stats.begin(); // 游戏逻辑 stats.end(); requestAnimationFrame(loop); }
  3. 自定义性能标记

    console.time('render'); // 渲染代码 console.timeEnd('render');

7. 从项目中学到的经验

实现这个Flappy Bird复刻版的过程中,有几个特别值得注意的教训:

  1. 物理参数的微调比预期中更重要 - 最初的重力值和跳跃力设置让游戏要么太难要么太简单,经过多次测试才找到平衡点。一个实用的调试方法是暴露这些参数到URL查询字符串中,方便快速调整测试。

  2. 移动端触摸延迟是个大问题。最初的click事件在手机上响应明显迟缓,后来改用touchstart并添加e.preventDefault()才解决。这个细节会极大影响游戏体验。

  3. Canvas绘制顺序容易出错。有次背景覆盖了分数显示,调试半天才发现是render调用顺序不对。现在我会在代码中明确注释绘制层次。

  4. 游戏状态管理随着功能增加变得越来越复杂。最初只用几个布尔标志,后来改用状态模式才让代码清晰起来。这也让我意识到即使是小游戏,良好的架构也很重要。

对于想要进一步挑战的开发者,可以尝试用TypeScript重写这个项目,静态类型检查能避免很多潜在错误。或者尝试使用WebGL渲染,虽然复杂度更高,但能学习到更底层的图形编程知识。

http://www.jsqmd.com/news/985525/

相关文章:

  • LPC2930汽车MCU开发实战:ARM9架构、CAN/LIN通信与电机控制详解
  • 2026实力之选:专业模温机与温度控制系统供应商精选概览 - 企业推荐官【官方】
  • 2026保姆级教程:Word文档怎么导出为图片?Windows/Mac/WPS通用方法 - 办公小帮手
  • 智能车竞赛新手必看:用GPS+IMU让越野车模跑起来(从PID调参到实战避坑)
  • AI驱动的临床评价数据筛选框架:构建可追溯、可验证、合规的数据证据链
  • 别再让数据库知道你查了什么:用Python和同态加密手把手实现一个简易PIR查询
  • 告别混乱!用IDEA + Gitee高效管理多人协作项目的完整配置流程
  • STK导弹弹道仿真实战:从Fixed Delta V模型到Python代码复现(含完整迭代算法解析)
  • 广安帝舵+浪琴手表专业回收,26年精选回收店铺排行榜推荐 - 莘州文化
  • 2026 成都金牛区黄金回收推荐 正规门店优选 - 禹竞
  • 广元帝舵+浪琴手表专业回收,26年精选回收店铺排行榜推荐 - 莘州文化
  • Mythos:首个具备语义级漏洞建模能力的AI安全模型
  • 深圳名表回收高奢首选,收的顶精收雅克德罗、伯爵 - 奢侈品回收测评
  • 别只跑回归了!用Stata的graph twoway命令画出更专业的学术图表(附异方差诊断)
  • 2026快手视频怎么去掉水印?快手自带去水印功能与合法方法详解 - 科技热点发布
  • K210硬核玩法:抛开Arduino思维,深入理解FPIOA机制与GPIO中断配置
  • 机器学习生产化:从Notebook到高可靠ML系统的核心实践
  • STM32 DMA2D不止能画矩形:手把手教你实现图片格式转换、Alpha混合与动画特效
  • 家装避坑指南,2026嘉兴全屋定制品牌推荐 - 高定
  • 从无人机航拍到自动驾驶:深入聊聊GNSS定位精度的‘隐形裁判’——DOP
  • 2026年装修必备!口碑爆棚的极简玻璃门厂家究竟哪家强? - 速递信息
  • 广州帝舵+浪琴手表专业回收,26年精选回收店铺排行榜推荐 - 莘州文化
  • Anthropic零层架构:用system指令替代中间件的AI工程范式革命
  • 2026 武汉汉口名包回收实测,商场专柜 vs 专业回收优劣对比 - 奢侈品交易观察员
  • 告别卡顿!用IDEA远程开发功能,让旧笔记本也能流畅跑SpringBoot项目
  • 别再只看GPS信号强度了!手把手教你读懂手机/车载导航里的DOP值(精度衰减因子)
  • 什么是敏捷思维
  • 合肥6月黄金回收口碑榜单:多次匿名探店,家门口对标大盘价靠谱门店盘点 - 禹竞
  • 避开这些坑!用QRCT做蓝牙射频测试时,90%的人都会犯的5个错误
  • 别让DRC吓到你!Cadence OrCAD 17.4中这5个“假警告”其实可以关掉