使用canvas写一个flappy bird小游戏

news2024/9/23 19:27:40

简介

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.innerWidth
    canvas.height = window.innerHeight
​
    window.addEventListener('resize', () => {
        canvas.width = window.innerWidth
        canvas.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 = 0
    this.image = SourceManager.images.bg_day
    this.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组,所以这里取大于3
    if (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组,所以这里取大于3
    if (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的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/355439.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

XSS注入进阶练习篇(一)XSS-LABS通关教程

XSS注入进阶练习篇1.常用标签整理2. XSS-LABS 练习2.1 level 1 无限制2.2 level 2 双引号闭合2.3 level 3 源码函数书写不全&#xff0c;单引号绕过2.4 level 4 无尖括号绕过2.5 level 5 a标签使用2.6 level 6 大小写绕过2.7 level 7 置空替换绕过2.8 level 8 URL编码绕过 - 重…

安全—07day

Tomcat AJP 文件包含漏洞&#xff08;CVE-2020- 1938&#xff09; 漏洞概述 Ghostcat(幽灵猫&#xff09;是由长亭科技安全研究员发现的存在于Tomcat 中的安全漏洞&#xff0c;由于Tomcat AJP 协议设计上存在缺陷,攻击者通过Tomcat AJP Connector可以读取或包含 Tomcat上所有…

Java岗面试题--Java并发(日积月累,每日三题)

目录面试题一&#xff1a;并行和并发有什么区别&#xff1f;面试题二&#xff1a;线程和进程的区别&#xff1f;追问&#xff1a;守护线程是什么&#xff1f;面试题三&#xff1a;创建线程的几种方式&#xff1f;1. 继承 Thread 类创建线程&#xff0c;重写 run() 方法2. 实现 …

详解垃圾回收算法,优缺点是什么?|金三银四系列

本文详细介绍了在 JVM 中如何判断哪些对象是需要回收的&#xff0c;以及不同的垃圾回收算法以及优缺点。点击上方“后端开发技术”&#xff0c;选择“设为星标” &#xff0c;优质资源及时送达上篇文章详细介绍了 JVM 的结构以及其内存结构&#xff0c;需要阅读请移步。本文主要…

Android 9.0系统源码_通知服务(二)应用发送状态栏通知的流程

前言 应用发送一个显示在状态栏上的通知&#xff0c;对于移动设备来说是很常见的一种功能需求&#xff0c;本篇文章我们将会结合Android9.0系统源码具体来分析一下&#xff0c;应用调用notificationManager触发通知栏通知功能的源码流程。 一、应用触发状态栏通知 应用可以通…

关于HDFS

目录 一、HDFS概述 二、HDFS架构与工作机制 三、HDFS的Shell操作 四、Hdfs的API操作 一、HDFS概述 HDFS&#xff1a;Hadoop Distributed File System&#xff1b;一种分布式文件管理系统&#xff0c;通过目录树定位文件。使用场景&#xff1a;一次写入&#xff0c;多次读出…

java 自定义注解

文章目录前言Annotation包自定义注解自定义注解示例参考文章&#xff1a;java 自定义注解 用处_java注解和自定义注解的简单使用参考文章&#xff1a;java中自定义注解的作用和写法前言 在使用Spring Boot的时候&#xff0c;大量使用注解的语法去替代XML配置文件&#xff0c;十…

SpringAMQP消息队列(SpringBoot集成RabbitMQ)

一、初始配置1、导入maven坐标<!--rabbitmq--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency>2、yml配置spring:rabbitmq:host: 你的rabbitmq的ipport: …

4G模块DTU网关远程抄表方案(三):水表188协议

4G模块DTU网关远程抄表方案&#xff08;三&#xff09;&#xff1a;水气电表188协议 1 CTJ 188协议简介 CJ/T188协议规定了户用计量仪表(以下简称仪表)&#xff0c;包括水表、燃气表、热量表等仪表数据传输的基本原则&#xff0c;接口形式及物理性能、数据链路、数据标识及数…

目标检测回归损失函数简介:SmoothL1/IoU/GIoU/DIoU/CIoU Loss

目标检测 回归损失函数1、Smooth L1 Loss2、 IoU Loss3、 GIoU Loss &#xff08;Generalized-IoU Loss&#xff09;4、 DIoU Loss &#xff08;Distance-IoU Loss&#xff09;5、 CIoU Loss &#xff08;Complete-IoU Loss&#xff09;总结&#xff1a;目标检测任务的损失函数…

【计算机网络】数据链路层(下)

文章目录媒体接入控制媒体接入控制-静态划分信道随机接入 CSMACD协议随机接入 CSMACA协议MAC地址MAC地址作用MAC地址格式MAC地址种类MAC地址的发送顺序单播MAC地址广播MAC地址多播MAC地址随机MAC地址IP地址区分网络编号IP地址与MAC地址的封装位置转发过程中IP地址与MAC地址的变…

1.1 硬件与micropython固件烧录及自编译固件

1.ESP32硬件和固件 淘宝搜ESP32模块,20-50元都有,自带usb口,即插即用. 固件下载地址:MicroPython - Python for microcontrollers 2.烧录方法 为简化入门难度,建议此处先使用带GUI的开发工具THonny,记得不是给你理发的tony老师. 烧录的入口是: 后期通过脚本一次型生成和烧…

[软件工程导论(第六版)]第3章 需求分析(课后习题详解)

文章目录1. 为什么要进行需求分析&#xff1f;通常对软件系统有哪些需求&#xff1f;2. 怎样与用户有效地沟通以获取用户的真实需求&#xff1f;3. 银行计算机储蓄系统的工作过程大致如下&#xff1a;储户填写的存款单或取款单由业务员输入系统&#xff0c;如果是存款则系统记录…

C语言经典编程题100例(81~100)

目录81、习题7-7 字符串替换82、习题8-10 输出学生成绩83、习题8-2 在数组中查找指定元素84、习题8-3 数组循环右移85、题8-9 分类统计各类字符个数86、习题9-2 计算两个复数之积87、习题9-6 按等级统计学生成绩88、习题11-1 输出月份英文名89、习题11-2 查找星期90、练习10-1 …

分享113个HTML娱乐休闲模板,总有一款适合您

分享113个HTML娱乐休闲模板&#xff0c;总有一款适合您 113个HTML娱乐休闲模板下载链接&#xff1a;https://pan.baidu.com/s/1aWYO2j2pSTjyqlQPHa0-Jw?pwdbium 提取码&#xff1a;bium Python采集代码下载链接&#xff1a;采集代码.zip - 蓝奏云 海上的沤鸟HTML网页模板…

(三十六)Vue解决Ajax跨域问题

文章目录环境准备vue的跨域问题vue跨域问题解决方案方式一方式二上一篇&#xff1a;&#xff08;三十五&#xff09;Vue之过渡与动画 环境准备 首先我们要借助axios发送Ajax&#xff0c;axios安装命令&#xff1a;npm i axios 其次准备两台服务器&#xff0c;这里使用node.j…

Linux | 网络通信 | 序列化和反序列化的讲解与实现

文章目录为什么要序列化&#xff1f;协议的实现服务端与客户端代码实现为什么要序列化&#xff1f; 由于默认对齐数的不同&#xff0c;不同的平台对相同数据进行内存对齐后&#xff0c;可能得到不同的数据。如果直接将这些数据进行网络传输&#xff0c;对方很可能无法正确的获…

【数据结构】单链表的接口实现(附图解和源码)

单链表的接口实现&#xff08;附图解和源码&#xff09; 文章目录单链表的接口实现&#xff08;附图解和源码&#xff09;前言一、定义结构体二、接口实现&#xff08;附图解源码&#xff09;1.开辟新空间2.头插数据3.头删数据4.打印整个单链表5.尾删数据6.查找单链表中的数据7…

Linux 磁盘挂载

目录 Linux硬盘分区 硬盘设备的文件名 /dev/sd[a-z] 硬盘分区 识别硬盘的文件名 Linux文件系统 文件系统类型 Linux如何保存文件 VFS虚拟文件系统 磁盘挂载命令 lsblk 查看系统的磁盘使用情况 fdisk 硬盘分区 mkfs 格式化文件系统 mount 挂载命令 df 显示磁盘空间…

Java中的链表实现介绍

Java中的链表实现介绍 学习数据结构的的链表和树时&#xff0c;会遇到节点&#xff08;node&#xff09;和链表&#xff08;linked list&#xff09;这两个术语&#xff0c;节点是处理数据结构的链表和树的基础。节点是一种数据元素&#xff0c;包括两个部分&#xff1a;一个是…