使用canvas写一个flappy bird小游戏
简介
canvas 是HTML5 提供的一种新标签,它可以支持 JavaScript 在上面绘画,控制每一个像素,它经常被用来制作小游戏,接下来我将用它来模仿制作一款叫flappy bird的小游戏。flappy bird(中文名:笨鸟先飞)是一款由来自越南的独立游戏开发者Dong Nguyen所开发的作品,于2013年5月24日上线,并在2014年2月突然暴红。
游戏规则
玩家只需要用一根手指来操控,点击或长按屏幕,小鸟就会往上飞,不断的点击就会不断的往高处飞。放松手指,则会快速下降。所以玩家要控制小鸟一直向前飞行,然后注意躲避途中高低不平的管子。小鸟安全飞过的距离既是得分。当然撞上就直接挂掉,只有一条命。
游戏素材
链接:pan.baidu.com/s/1JZR27H1K…
提取码:02ii
开始制作
初始化canvas画布
这里主要是创建画布,并调整画布大小,画布自适应屏幕大小。
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><style> body {margin: 0;padding: 0;overflow: hidden;} </style>
</head>
<body><canvas id="canvas">当前浏览器不支持canvas,请更换浏览器查看。</canvas>
<script> /** @type {HTMLCanvasElement} */
const canvas = document.querySelector('#canvas')const ctx = canvas.getContext('2d')
canvas.width = window.innerWidthcanvas.height = window.innerHeight
window.addEventListener('resize', () => {canvas.width = window.innerWidthcanvas.height = window.innerHeight}) </script>
</body>
</html>
加载资源
图片等资源的加载是异步的,只有当所有的资源都加载完了才能开始游戏,所以这里需要对图片等资源进行统一的监控和管理。 将图片资源用json进行描述,通过fetch进行统一加载。
// 资源管理器
class SourceManager {static images = {};static instance = new SourceManager();constructor() {return SourceManager.instance;}
loadImages() {return new Promise((resolve) => {fetch("./assets/images/image.json").then((res) => res.json()).then((res) => {res.forEach((item, index) => {const image = new Image();image.src = item.url;image.onload = () => {SourceManager.images[item.name] = image;ctx.clearRect(0, 0, canvas.width, canvas.height);ctx.font = "24px 黑体";ctx.textAlign = "center";ctx.fillText(`资源加载中${index + 1}/${res.length}...`, canvas.width / 2, (canvas.height / 2) * 0.618);if (index === res.length - 1) {console.log(index, "加载完成");resolve();}};});});});}
}
async function main() {// 加载资源await new SourceManager().loadImages();
}
main();
背景
为了适应不同尺寸的屏幕尺寸和管子能正确渲染到对应的位置,不能将背景图片拉伸,要定一个基准线固定背景图片所在屏幕中的位置。我们发现背景图并不能充满整个画面,上右下面是空缺的,这个时候需要使用小手段填充上,这里就用矩形对上部进行填充。接下来,需要让背景有一种无限向左移动的效果,就要并排绘制3张背景图片,这样在渲染的时候,当背景向左移动的距离dx等于一张背景图的宽度时,将dx=0,这样就实现了无限向左移动的效果,类似于轮播图。
// 背景
class GameBackground {constructor() {this.dx = 0this.image = SourceManager.images.bg_daythis.dy = 0.8 * (canvas.height - this.image.height)this.render()}
update() {this.dx -= 1 if (this.dx + this.image.width <= 0) {this.dx = 0}this.render()}
render() {ctx.fillStyle = '#4DC0CA'ctx.fillRect(0, 0, canvas.width, 0.8 * (canvas.height - this.image.height) + 10)ctx.drawImage(this.image, this.dx, this.dy)ctx.drawImage(this.image, this.dx + this.image.width, this.dy)ctx.drawImage(this.image, this.dx + this.image.width * 2, this.dy)}
}
let gameBg = null
main();
// 渲染函数
function render() {ctx.clearRect(0, 0, canvas.width, canvas.height);gameBg.update();requestAnimationFrame(render)
}
async function main() {// 加载资源await new SourceManager().loadImages();
// 背景gameBg = new GameBackground()
// 渲染动画render()
}
地面
地面要在背景的基础上将地面图上边对齐基准线(canvas.height * 0.8),并把下面空缺的部分通过和填补背景上半部分一致的方式填上。同时使用与背景无限向左移动一样的方法实现地面的无限向左移动。
// 地面
class Land {constructor() {this.dx = 0;this.dy = canvas.height * 0.8;this.image = SourceManager.images.land;this.render();}
update() {this.dx -= 1.5;if (this.dx + this.image.width <= 0) {this.dx = 0;}this.render();}
render() {ctx.fillStyle = "#DED895";ctx.fillRect(0,canvas.height * 0.8 + this.image.height - 10,canvas.width,canvas.height * 0.2 - this.image.height + 10);ctx.drawImage(this.image, this.dx, this.dy);ctx.drawImage(this.image, this.dx + this.image.width, this.dy);ctx.drawImage(this.image, this.dx + this.image.width * 2, this.dy);}
}
let land = null
main();
// 渲染函数
function render() {ctx.clearRect(0, 0, canvas.width, canvas.height);gameBg.update();requestAnimationFrame(render)
}
async function main() {// 加载资源await new SourceManager().loadImages();
// 此处省略其他元素// 地面land = new Land()
// 渲染动画render()
}
管道
管道有上下两部分,上部分管道需要贴着屏幕的顶部渲染,下部分要贴着地面也就是基准线渲染,上下两部分的管道长度要随机生成,且两部分之间的距离不能小于80(我自己限制的);管道渲染速度为2s一次,并且也需要无限向左移动,这个效果和背景同理。
// 管道
class Pipe {constructor() {this.dx = canvas.width;this.dy = 0;this.upPipeHeight = (Math.random() * canvas.height * 0.8) / 2 + 30;this.downPipeHeight = (Math.random() * canvas.height * 0.8) / 2 + 30;
if (canvas.height * 0.8 - this.upPipeHeight - this.downPipeHeight <= 80) {console.log("///小于80了///");this.upPipeHeight = 200;this.downPipeHeight = 200;}
this.downImage = SourceManager.images.pipe_down;this.upImage = SourceManager.images.pipe_up;}
update() {this.dx -= 1.5;// 记录管道四个点的坐标,在碰撞检测的时候使用this.upCoord = {tl: {x: this.dx,y: canvas.height * 0.8 - this.upPipeHeight,},tr: {x: this.dx + this.upImage.width,y: canvas.height * 0.8 - this.upPipeHeight,},bl: {x: this.dx,y: canvas.height * 0.8,},br: {x: this.dx + this.upImage.width,y: canvas.height * 0.8,},};this.downCoord = {bl: {x: this.dx,y: this.downPipeHeight,},br: {x: this.dx + this.downImage.width,y: this.downPipeHeight,},};this.render();}
render() {ctx.drawImage(this.downImage,0,this.downImage.height - this.downPipeHeight,this.downImage.width,this.downPipeHeight,this.dx,this.dy,this.downImage.width,this.downPipeHeight);ctx.drawImage(this.upImage,0,0,this.upImage.width,this.upPipeHeight,this.dx,canvas.height * 0.8 - this.upPipeHeight,this.upImage.width,this.upPipeHeight);}
}
let pipeList = []
main();function render() {ctx.clearRect(0, 0, canvas.width, canvas.height);// 此处省略其他元素渲染步骤
pipeList.forEach((item) => item.update());requestAnimationFrame(render)
}
async function main() {// 此处省略其他元素渲染步骤// 管道setInterval(() => {pipeList.push(new Pipe());
// 清理移动过去的管道对象,一屏最多展示3组,所以这里取大于3if (pipeList.length > 3) {pipeList.shift();}}, 2000);
// 渲染动画render()
}
笨鸟
小鸟要有飞行的动作,这个通过不断重复渲染3张小鸟不同飞行姿势的图片来实现;还要通过改变小鸟的在Y轴的值来制作上升下坠的效果,并且能够通过点击或长按屏幕来控制小鸟的飞行高度。
// 小鸟
class Bird {constructor() {this.dx = 0;this.dy = 0;this.speed = 2;this.image0 = SourceManager.images.bird0_0;this.image1 = SourceManager.images.bird0_1;this.image2 = SourceManager.images.bird0_2;
this.loopCount = 0;
this.control();
setInterval(() => {if (this.loopCount === 0) {this.loopCount = 1;} else if (this.loopCount === 1) {this.loopCount = 2;} else {this.loopCount = 0;}}, 200);}
// 添加控制小鸟的事件control() {let timer = true;canvas.addEventListener("touchstart", (e) => {timer = setInterval(() => {this.dy -= this.speed;});e.preventDefault();});canvas.addEventListener("touchmove", () => {clearInterval(timer);});canvas.addEventListener("touchend", () => {clearInterval(timer);});}
update() {this.dy += this.speed;
// 记录小鸟四个点的坐标,在碰撞检测的时候使用this.birdCoord = {tl: {x: this.dx,y: this.dy,},tr: {x: this.dx + this.image0.width,y: this.dy,},bl: {x: this.dx,y: this.dy + this.image0.height,},br: {x: this.dx + this.image0.width,y: this.dy + this.image0.height,},};
this.render();}
render() {// 渲染小鸟飞行动作if (this.loopCount === 0) {ctx.drawImage(this.image0, this.dx, this.dy);} else if (this.loopCount === 1) {ctx.drawImage(this.image1, this.dx, this.dy);} else {ctx.drawImage(this.image2, this.dx, this.dy);}}
}
let bird = null
main();function render() {ctx.clearRect(0, 0, canvas.width, canvas.height);// 省略其他元素渲染bird.update();requestAnimationFrame(render);
}
async function main() {// 省略其他元素渲染// 笨鸟bird = new Bird()
// 渲染动画render()
}
我们发现小鸟好像是只美国鸟,有点太freedom了~,不符合我们的游戏规则,要想办法控制一下。
碰撞检测
碰撞检测的原理就是不断检测小鸟图四个顶点坐标是否在任一管道所占的坐标区域内或小鸟图下方的点纵坐标小于地面纵坐标(基准线),在就结束游戏。上面管道和小鸟类中记录的坐标就是为了实现碰撞检测的。
let gameBg = null
let land = null
let bird = null
let pipeList = []
main();function render() {ctx.clearRect(0, 0, canvas.width, canvas.height);gameBg.update();land.update();bird.update();pipeList.forEach((item) => item.update());requestAnimationFrame(render);
// 碰撞检测-地面if (bird.dy >= canvas.height * 0.8 - bird.image0.height + 10) {gg();}//碰撞检测-管道pipeList.forEach((item) => {if (bird.birdCoord.bl.x >= item.upCoord.tl.x - 35 &&bird.birdCoord.bl.x <= item.upCoord.tr.x &&bird.birdCoord.bl.y >= item.upCoord.tl.y + 10) {gg();} else if (bird.birdCoord.tl.x >= item.downCoord.bl.x - 35 &&bird.birdCoord.tl.x <= item.downCoord.br.x &&bird.birdCoord.tl.y <= item.downCoord.bl.y - 10) {gg();}});
}
async function main() {// 加载资源await new SourceManager().loadImages();
// 背景gameBg = new GameBackground()
// 地面land = new Land()
// 笨鸟bird = new Bird()
// 管道setInterval(() => {pipeList.push(new Pipe());
// 清理移动过去的管道对象,一屏最多展示3组,所以这里取大于3if (pipeList.length > 3) {pipeList.shift();}}, 2000);
// 渲染动画render()
}
function gg() {const ggImage = SourceManager.images.text_game_over;ctx.drawImage(ggImage,canvas.width / 2 - ggImage.width / 2,(canvas.height / 2) * 0.618);
};
效果
增加碰撞检测后,小鸟碰到管道或地面就会提示失败。 此篇展示了基本的核心逻辑,完整游戏地址和源码在下方链接。
最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。
有需要的小伙伴,可以点击下方卡片领取,无偿分享
相关文章:

使用canvas写一个flappy bird小游戏
简介 canvas 是HTML5 提供的一种新标签,它可以支持 JavaScript 在上面绘画,控制每一个像素,它经常被用来制作小游戏,接下来我将用它来模仿制作一款叫flappy bird的小游戏。flappy bird(中文名:笨鸟先飞&am…...
KVM-2、虚拟化基础
1. 虚拟化概念 什么是虚拟化 **虚拟化是使用所谓虚拟机管理程序从一台物理机上创建若干个虚拟机的过程。**虚拟机的行为和运转方式与物理机一样,但它们会使用物理机的计算资源,如 CPU 、内存和存储。虚拟机管理程序会根据需要将这些计算资源分配给每个虚拟机。 虚拟化有哪…...

设计模式之观察者模式与访问者模式详解和应用
目录1.访问者模式详解1.1 访问者模式的定义1.1.1 访问者模式在生活中的体现1.1.2 访问者模式的适用场景1.2 访问者模式的通用实现1.3 访问者模式的使用案例之KPI考核1.3.1 类图设计1.3.2 代码实现1.4 访问者模式扩展---分派1.4.1 java中静态分派示例代码1.4.2 java中动态分派1.…...

spring注解方式整合Dubbo源码解析
系列文章目录 前言 本节我们的Dubbo源码版本基于2.6.x 在前一章我们的整合案例中,我们有几个比较关键的步骤: 在启动类上标注了EnableDubbo注解在provider类上面标注了Service注解来提供dubbo服务在消费的时候通过Reference注解引入dubbo服务在配置文件…...

大数值金额大写转换(C语言)
关于大数值金额大写转换,在财务管理的应用方面没什么意义。一般来说,千亿级,万亿级的数值就够了。因为在国家级层面是以亿为单位的,也就表达为千万亿,万万亿。在企业层面数值金额转换设置到千亿、万亿就行了。大的集团…...

迷宫问题图解 : 基于骨架提取、四邻域
目录 1. 迷宫的连通域 2. How to remove branch ? 3. 基于4邻域的 remove 分支 3.1 找到分支的端点 3.2 4邻域的 remove 分支 3.3 循环移除分支 3.4 code 4. 迷宫路线 4.1 预处理 4.2 提取骨架 4.3 分支的端点 4.4 去除分支的端点 4.5 循环去除分支 4…...
设计模式 - 如何在库和主程序之间互相调用数据和函数
背景:在项目开发过程中,难免碰到这种情况,当我们想要通过我们开发的库,调用主程序中的一些变量或者函数的时候,就会导致一些问题,因为在项目构建过程中,库都是不依赖于主程序编译的,…...

Redis面试题:1~2亿条数据需要缓存,请问如何设计这个存储案例
目录 前言 一、哈希取余分区 优点 缺点 二、一致性哈希算法分区 背景 步骤 ① 算法构建一致性哈希环 ② 服务器IP节点映射 ③ key落到服务器的落键规则 优点 ① 容错性 ② 扩展性 缺点 三、哈希槽分区 前言 单机单台100%不可能,肯定是分布式存储&am…...
程序员必备的软技能-《如何阅读一本书》
阅读很重要,我们真的会阅读吗? 这本书的初版是 1940年,时隔 80年,其内容仍然不过时。第一次读这本书时,给我最大的影响就是主题阅读,每次学习一个新理论、技术,都入手多本关于这项理论、技术的书…...

Java数据结构-栈、队列常用类(Stack、ArrayDeque、LinkedLList)
数据结构的三要素包括:逻辑结构、存储结构、数据的运算。逻辑结构描述的是数据之间的逻辑关系,分为线性结构(线性表(数组、链表)、栈、队列)和非线性结构(图、树、集合)。物理结构也…...

拯救了大批爬虫程序员,因为一个简单的神器
相信大家应该都写过爬虫,简单的爬虫只需要使用 requests 即可。遇到复杂的爬虫,就需要在程序里面加上请求头和参数信息。类似这种:我们一般的步骤是,先到浏览器的网络请求中找到我们需要的请求,然后将请求头和参数信息…...

2023年美赛C题Wordle预测问题三、四建模及Python代码详细讲解
更新时间:2023-2-19 16:30 相关链接 (1)2023年美赛C题Wordle预测问题一建模及Python代码详细讲解 (2)2023年美赛C题Wordle预测问题二建模及Python代码详细讲解 (3)2023年美赛C题Wordle预测问题三、四建模…...

相关性-回忆录(持续更新)
1.TODO方向 (1)数据增强:finetuning阶段需要大量人工标注样本,消耗时间和成本。用户点击数据作为弱监督学习,可以尝试图网络构建节点和边(query聚合); 使用展现未点击生成对抗网络进…...
(必备技能)使用Python实现屏幕截图
(必备技能)使用Python实现屏幕截图 文章目录 (必备技能)使用Python实现屏幕截图 一、序言二、环境配置 1、下载pyautogui包2、下载opencv-python包3、下载PyQt5包4、下载pypiwin32包 三、屏幕截屏源码与解析 1、使用pyautogui方法实现截屏2、使用PyQt方法实现截屏 a.获取窗口…...

「数据仓库」怎么选择现代数据仓库?
构建自己的数据仓库时要考虑的基本因素我们用过很多数据仓库。当我们的客户问我们,对于他们成长中的公司来说,最好的数据仓库是什么时,我们会根据他们的具体需求来考虑答案。通常,他们需要几乎实时的数据,价格低廉&…...

6.3 使用 Swagger 生成 Web API 文档
第6章 构建 RESTful 服务 6.1 RESTful 简介 6.2 构建 RESTful 应用接口 6.3 使用 Swagger 生成 Web API 文档 6.4 实战:实现 Web API 版本控制 6.3 使用 Swagger 生成 Web API 文档 高质量的 API 文档在系统开发的过程中非常重要。本节介绍什么是 Swaggerÿ…...

Day894.加锁规则的一些问题 -MySQL实战
加锁规则的一些问题 Hi,我是阿昌,今天学习记录的是关于加锁规则的一些问题的内容。 加锁规则,这个规则中,包含了两个“原则”、两个“优化”和一个“bug”: 原则 1:加锁的基本单位是 next-key lock。nex…...

【Flutter入门到进阶】Dart进阶篇---Dart异步编程
1 并行与并发的编程区别 1.1 并发与并行 1.1.1 说明 我们举个例子,如果有条高速公路 A 上面并排有 8 条车道,那么最大的并行车辆就是 8 辆此条高速公路 A 同时并排行走的车辆小于等于 8 辆的时候,车辆就可以并行运行。 CPU 也是这个原理,一个 CPU 相当于一个高速公路 A,核心数…...

点云配准方法原理(NDT、ICP)
配准是点云处理中的一个基础问题,众多学者此问题进行了广泛而深入的研究,也出现了一系列优秀成熟的算法,在三维建模、自动驾驶等领域发挥着重要的作用。 本文主要介绍粗配准NDT (Normal Distribution Transform) 与 精配准ICP (Iterative Cl…...
STM32开发,创建线程栈空间大小判断
1. 使用RTOS提供的API函数(以FreeRTOS为例) 函数原型:UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask)功能:获取指定任务堆栈中剩余的最小空间(以字为单位,非字节)。使用步骤&am…...

51单片机基础部分——矩阵按键检测
前言 上一节,我们说到了独立按键的检测以及使用,但是独立按键每一个按键都要对应一个IO口进行检测,在一些需要多按键的情况下,使用过多的独立按键会过多的占用单片机的IO资源,为了解决这个问题的出现,我们…...

基于功能基团的3D分子生成扩散模型 - D3FG 评测
D3FG 是一个在口袋中基于功能团的3D分子生成扩散模型。与通常分子生成模型直接生成分子坐标和原子类型不同,D3FG 将分子分解为两类组成部分:官能团和连接体,然后使用扩散生成模型学习这些组成部分的类型和几何分布。 一、背景介绍 D3FG 来源…...
DHCP 动态主机配置协议(Dynamic host configuration protocol)逐层封装过程: DHCP --> UDP --> IP
📦 DHCP 报文逐层封装结构(自上而下) 应用层(DHCP 报文) ↓ 传输层(UDP 首部) ↓ 网络层(IP 首部) ↓ 数据链路层(以太网帧头) ↓ 物理层&#x…...

微软PowerBI考试 PL300-使用适用于 Power BI 的 Copilot 创建交互式报表
微软PowerBI考试 PL300-使用适用于 Power BI 的 Copilot 创建交互式报表 Microsoft Power BI 可帮助您通过交互式报表准备数据并对数据进行可视化。 如果您是 Power BI 的新用户,可能很难知道从哪里开始,并且创建报表可能很耗时。 通过适用于 Power BI …...

开源之夏·西安电子科技大学站精彩回顾:OpenTiny开源技术下沉校园,点燃高校开发者技术热情
开源之夏2025编程活动正在如火如荼的进行中,当前也迎来了报名的倒计时阶段,开源之夏组织方也通过高校行系列活动进入各大高校,帮助高校开发者科普开源文化、开源活动、开源技术。 6月4日 开源之夏携手多位开源技术大咖、经验型选手走进西安电…...
多模型协同:基于 SAM 分割 + YOLO 检测 + ResNet 分类的工业开关状态实时监控方案
一、技术优势与适配性分析 1. 任务分工的合理性 YOLO(目标检测) 核心价值:快速定位工业开关在图像中的位置(边界框),为后续分割和分类提供ROI(感兴趣区域)。工业场景适配性…...

视觉SLAM基础补盲
3D Gaussian Splatting for Real-Time Radiance Field Rendering SOTA方法3DGS contribution传统重建基于点的渲染NeRF 基础知识补盲光栅化SFM三角化极线几何标准的双目立体视觉立体匹配理论与方法立体匹配的基本流程李群和李代数 李群和李代数的映射李代数的求导李代数解决求导…...

vue+cesium示例:地形开挖(附源码下载)
基于cesium和vue绘制多边形实现地形开挖效果,适合学习Cesium与前端框架结合开发3D可视化项目。 demo源码运行环境以及配置 运行环境:依赖Node安装环境,demo本地Node版本:推荐v18。 运行工具:vscode或者其他工具。 配置方式&#x…...

基于SFC的windows系统损坏修复程序
前言 在平时使用Windows操作系统时会遇到很多因为系统文件损坏而出现的错误 例如:系统应用无法打开 系统窗口(例如开始菜单)无法使用 电脑蓝屏或者卡死 是如果想要修复很多人只能想到重装系统。但其实Windows有一个内置的系统文件检查器可以修复此类错误。 原理 SFC命令…...