使用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…...

Docker 离线安装指南
参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性,不同版本的Docker对内核版本有不同要求。例如,Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本,Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...
java_网络服务相关_gateway_nacos_feign区别联系
1. spring-cloud-starter-gateway 作用:作为微服务架构的网关,统一入口,处理所有外部请求。 核心能力: 路由转发(基于路径、服务名等)过滤器(鉴权、限流、日志、Header 处理)支持负…...

如何将联系人从 iPhone 转移到 Android
从 iPhone 换到 Android 手机时,你可能需要保留重要的数据,例如通讯录。好在,将通讯录从 iPhone 转移到 Android 手机非常简单,你可以从本文中学习 6 种可靠的方法,确保随时保持连接,不错过任何信息。 第 1…...

SpringCloudGateway 自定义局部过滤器
场景: 将所有请求转化为同一路径请求(方便穿网配置)在请求头内标识原来路径,然后在将请求分发给不同服务 AllToOneGatewayFilterFactory import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; impor…...

深度学习水论文:mamba+图像增强
🧀当前视觉领域对高效长序列建模需求激增,对Mamba图像增强这方向的研究自然也逐渐火热。原因在于其高效长程建模,以及动态计算优势,在图像质量提升和细节恢复方面有难以替代的作用。 🧀因此短时间内,就有不…...

基于IDIG-GAN的小样本电机轴承故障诊断
目录 🔍 核心问题 一、IDIG-GAN模型原理 1. 整体架构 2. 核心创新点 (1) 梯度归一化(Gradient Normalization) (2) 判别器梯度间隙正则化(Discriminator Gradient Gap Regularization) (3) 自注意力机制(Self-Attention) 3. 完整损失函数 二…...
第7篇:中间件全链路监控与 SQL 性能分析实践
7.1 章节导读 在构建数据库中间件的过程中,可观测性 和 性能分析 是保障系统稳定性与可维护性的核心能力。 特别是在复杂分布式场景中,必须做到: 🔍 追踪每一条 SQL 的生命周期(从入口到数据库执行)&#…...
SpringAI实战:ChatModel智能对话全解
一、引言:Spring AI 与 Chat Model 的核心价值 🚀 在 Java 生态中集成大模型能力,Spring AI 提供了高效的解决方案 🤖。其中 Chat Model 作为核心交互组件,通过标准化接口简化了与大语言模型(LLM࿰…...
微服务通信安全:深入解析mTLS的原理与实践
🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、引言:微服务时代的通信安全挑战 随着云原生和微服务架构的普及,服务间的通信安全成为系统设计的核心议题。传统的单体架构中&…...

实战设计模式之模板方法模式
概述 模板方法模式定义了一个操作中的算法骨架,并将某些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的前提下,重新定义算法中的某些步骤。简单来说,就是在一个方法中定义了要执行的步骤顺序或算法框架,但允许子类…...