使用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…...
Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...
Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...
Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)
概述 在 Swift 开发语言中,各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过,在涉及到多个子类派生于基类进行多态模拟的场景下,…...
Ascend NPU上适配Step-Audio模型
1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤)&#x…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...
稳定币的深度剖析与展望
一、引言 在当今数字化浪潮席卷全球的时代,加密货币作为一种新兴的金融现象,正以前所未有的速度改变着我们对传统货币和金融体系的认知。然而,加密货币市场的高度波动性却成为了其广泛应用和普及的一大障碍。在这样的背景下,稳定…...
Java求职者面试指南:计算机基础与源码原理深度解析
Java求职者面试指南:计算机基础与源码原理深度解析 第一轮提问:基础概念问题 1. 请解释什么是进程和线程的区别? 面试官:进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位;而线程是进程中的…...
MySQL JOIN 表过多的优化思路
当 MySQL 查询涉及大量表 JOIN 时,性能会显著下降。以下是优化思路和简易实现方法: 一、核心优化思路 减少 JOIN 数量 数据冗余:添加必要的冗余字段(如订单表直接存储用户名)合并表:将频繁关联的小表合并成…...
springboot 日志类切面,接口成功记录日志,失败不记录
springboot 日志类切面,接口成功记录日志,失败不记录 自定义一个注解方法 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;/***…...
mac:大模型系列测试
0 MAC 前几天经过学生优惠以及国补17K入手了mac studio,然后这两天亲自测试其模型行运用能力如何,是否支持微调、推理速度等能力。下面进入正文。 1 mac 与 unsloth 按照下面的进行安装以及测试,是可以跑通文章里面的代码。训练速度也是很快的。 注意…...
