JavaScript使用canvas实现flappy bird全流程详解

目录
  • 简介
  • 游戏规则
  • 游戏素材
  • 开始制作
    • 初始化canvas画布
    • 加载资源
    • 背景
    • 地面
    • 管道
    • 笨鸟
    • 碰撞检测
    • 效果

简介

canvas 是HTML5 提供的一种新标签,它可以支持 JavaScript 在上面绘画,控制每一个像素,它经常被用来制作小游戏,接下来我将用它来模仿制作一款叫flappy bird的小游戏。flappy bird(中文名:笨鸟先飞)是一款由来自越南的独立游戏开发者Dong Nguyen所开发的作品,于2013年5月24日上线,并在2014年2月突然暴红。

游戏规则

玩家只需要用一根手指来操控,点击或长按屏幕,小鸟就会往上飞,不断的点击就会不断的往高处飞。放松手指,则会快速下降。所以玩家要控制小鸟一直向前飞行,然后注意躲避途中高低不平的管子。小鸟安全飞过的距离既是得分。当然撞上就直接挂掉,只有一条命。

游戏素材

链接: https://pan.baidu.com/s/1ro1273TeIhhJgCIFj4vn_g?pwd=7vqh

提取码: 7vqh

开始制作

初始化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使用canvas实现flappy bird全流程详解的文章就介绍到这了,更多相关JS flappy bird内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • JavaScript+Canvas模拟实现支付宝画年兔游戏

    目录 动手前的思路 思考1.如何让鼠标只能在特定区域内画画? 思考2.如何让绘制的图画动起来 思考3.如何撤销上一步操作 思考4.如何判断线条绘制完毕 关键步骤 接近过年了,支付宝的集福的活动又开始了,集美们的五福集齐了没有.每年的集福活动都有一些小游戏,今年也不例外,画年画就是其中之一,本篇用canvas来写一个画年兔的游戏. 动手前的思路 画年画游戏规则是:跟着特定轮廓画出线条来. 思考1.如何让鼠标只能在特定区域内画画? 首先要获取到这个轮廓区域所在画布上的位置,判断鼠标绘画的位置是否在指

  • 利用JS+Canvas给南方的冬季来一场纷纷扬扬的大雪

    目录 前言 具体实现 1. 页面布局 2. 雪花的实现 3. 实现下雪 总结 前言 今年冬季都快接近尾声了,身处在南方的我,一点小雪花都还没见到.今年感觉也没以往的冬季冷,以往的冬季就不太能见到一场大雪,今年估计更不可能见到一场大雪纷飞的景色了,因此,用代码来实现一场纷纷扬扬的大雪,完成自己今年看雪的愿望. 具体实现 使用Canvas实现雪花纷飞的场景. 1. 页面布局 页面html,body 设置宽100%.高100vh,铺满整个屏幕,并设置一张好看的背景图或者背景色,能够很好地和白色的雪花相

  • JavaScript利用Canvas实现粒子动画倒计时

    目录 canvas 粒子动画介绍 何为canvas 粒子动画是啥 canvas 定义初始变量 初始化canvas和数字文本 创建一定数量的点 倒计时 倒计时文本绘画 循环绘制 点动画 效果图 canvas 粒子动画介绍 何为canvas canvas是HTML5中新增的一个标签,主要是用于网页实时生成图像并可操作图像,它是用JavaScript操作的bitmap. 粒子动画是啥 粒子动画就是页面上通过发射许多微小粒子来表示不规则模糊物体,比如:用小圆点来模拟下雪.下雨的效果,用模糊线条模拟黑客帝

  • JS技巧Canvas 性能优化脏矩形渲染实例详解

    目录 正文 画布该如何更新? 脏矩形渲染原理 脏矩形渲染实现 性能测试 结尾 正文 使用 Canvas 做图形编辑器时,我们需要自己维护自己的图形树,来保存图形的信息,并定义元素之间的关系. 我们改变画布中的某个图形,去更新画布,最简单的是清空画布,然后根据图形树将所有图形再绘制一遍,这在图形较少的情况下是没什么问题的.但如果图形数量很多,那绘制起来可能就出现卡顿了. 那么,有没有什么办法来优化一下?有,脏矩形渲染. 画布该如何更新? 这里我们假设这么一个场景,画布上绘制了随机位置大量的绿球,然

  • JavaScript+Canvas实现文字粒子流特效

    目录 动手前思考 绘制文字 获取像素点位 渲染粒子 简单的动画效果 1.随机选择四个方向中的某一个方向,生成初始坐标 2.从初始位置运动到实际位置 3.点击文字炸开的特效 动手前思考 首先要在特定的位置生成粒子,要获取到canvas上像素的点位,通过canvas的getImageData函数我们可以得到canvas像素点的信息,获取像素点中透明度大于0的位置. 绘制文字 新建一个canvas画布,在画布上绘制任意的文字 ctx.font = "200px Arial"; ctx.fon

  • JS+Canvas实现满屏爱心和文字动画的制作

    目录 介绍 步骤 介绍 <canvas> 最早由 Apple 引入 WebKit,用于 Mac OS X 的 Dashboard,随后被各个浏览器实现.如今,所有主流的浏览器都支持它.Canvas API 提供了一个通过 JavaScript 和 HTML 的 <canvas> 元素来绘制图形的方式.它可以用于动画.游戏画面.数据可视化.图片编辑以及实时视频处理等方面.Canvas 适合绘制大数据量图形元素的图表(如热力图.地理坐标系或平行坐标系上的大规模线图或散点图等),也适合实

  • SpringBoot解析yml全流程详解

    目录 背景 加载监听器 执行run方法 加载配置文件 封装Node 调用构造器 思考 背景 前几天的时候,项目里有一个需求,需要一个开关控制代码中是否执行一段逻辑,于是理所当然的在yml文件中配置了一个属性作为开关,再配合nacos就可以随时改变这个值达到我们的目的,yml文件中是这样写的: switch: turnOn: on 程序中的代码也很简单,大致的逻辑就是下面这样,如果取到的开关字段是on的话,那么就执行if判断中的代码,否则就不执行: @Value("${switch.turnOn}

  • Redis实现延迟队列的全流程详解

    目录 1.前言 1.1.什么是延迟队列 1.2.应用场景 1.3.为什么要使用延迟队列 2.Redis sorted set 3.Redis 过期键监听回调 4.Quartz定时任务 5.DelayQueue 延迟队列 6.RabbitMQ 延时队列 7.时间轮 1.前言 1.1.什么是延迟队列 延时队列相比于普通队列最大的区别就体现在其延时的属性上,普通队列的元素是先进先出,按入队顺序进行处理,而延时队列中的元素在入队时会指定一个延迟时间,表示其希望能够在经过该指定时间后处理.从某种意义上来讲

  • JavaScript实现Promise流程详解

    目录 构造函数 then 和 catch方法 解决异步问题 all和race方法 构造函数 首先我们来看一下我们是如何使用promise的,我们在实例化对象是这么使用的: let p1 = new Promise((resolve, reject) => { let random = Math.floor(Math.random() * 10); if (random > 4) { resolve('sucess') } else { reject('erro') } }) 所以我们在创建我们

  • JavaScript制作楼层导航效果流程详解

    目录 本期目标 1. 功能实现 1.1 结构层 1.2 样式层 1.3 行为层 1.3.1 楼层跳转 1.3.2 楼层监听 2. 效果预览 3. 项目代码 本期目标 使用JavaScript制作楼层导航效果,实现两个功能: 楼层跳转 楼层监听 1. 功能实现 1.1 结构层 <div id="box" class="box"> <ul class="list"> <li class="content-par

  • javascript jquery对form元素的常见操作详解

    1.下拉框 select : 移除option $("#ID option").each(function(){ if($(this).val() == 111){ $(this).remove(); } }); 添加option $("<option value='111'>UPS Ground</option>").appendTo($("#ID")); 取得下拉选单的选取值 //取下拉選中的文本 $('#testSe

  • Android view绘制流程详解

    绘制流程 measure 流程测量出 View 的宽高尺寸. layout 流程确定 View 的位置及最终尺寸. draw 流程将 View 绘制在屏幕上. Measure 测量流程 系统是通过 MeasureSpec 测量 View 的,在了解测量过程之前一定要了解这个 MeasureSpec . MeasureSpec MeasureSpec 是一个 32 位的 int 值打包而来的,打包为 MeasureSpec 主要是为了避免过多的对象内存分配. 为了方便操作,MeasureSpec

  • Android 实例开发一个学生管理系统流程详解

    目录 效果演示 实现功能总览 代码 登录与忘记密码界面 一.添加布局文件 二.添加标题文字 三.绑定适配器 注册界面 一.创建两个Drawable文件 二.将其添加数组内 三.动态变化背景 考勤界面 一.CircleProgressBar代码如下 签到界面 一.倒计时 二.位置签到 成绩查询界面 一.创建StackAdapter 适配器 效果演示 随手做的一个小玩意,还有很多功能没有完善,倘有疏漏,万望海涵. 实现功能总览 实现了登录.注册.忘记密码.成绩查询.考勤情况.课表查看.提交作业.课程

  • C++ 情怀游戏扫雷的实现流程详解

    扫雷最原始的版本可以追溯到1973年一款名为"方块"的游戏. 不久,"方块"被改写成了游戏"Rlogic".在"Rlogic"里,玩家的任务是作为美国海军陆战队队员,为指挥中心探出一条没有地雷的安全路线,如果路全被地雷堵死就算输.两年后,汤姆·安德森在"Rlogic"的基础上又编写出了游戏"地雷",由此奠定了现代扫雷游戏的雏形. 1981年,微软公司的罗伯特·杜尔和卡特·约翰逊两位工程师

  • jmeter接口测试教程及接口测试流程详解(全网仅有)

    目录 一.Jmeter简介 二.Jmeter安装 三.设置Jmeter语言为中文环境 四.Jmeter主要元件 五.Jmeter元件的作用域和执行顺序 六.Jmeter进行接口测试流程 七.Jmeter进行接口测试流程步骤详解 八.Jmeter接口测试必定用到的扩展阅读 一.Jmeter简介 Jmeter是由Apache公司开发的一个纯Java的开源项目,即可以用于做接口测试也可以用于做性能测试. Jmeter具备高移植性,可以实现跨平台运行. Jmeter可以实现分布式负载. Jmeter采用

  • JavaScript实现六种网页图片轮播效果详解

    目录 1.当鼠标经过轮播图模块,左右按钮显示,离开隐藏左右按钮. 2.动态生成小圆圈 3.点击小圆圈,小圆圈变色 4.点击小圆圈滚动图片 5.点击右侧按钮一次,就让图片滚动一张. 6.点击右侧按钮, 小圆圈跟随变化 7.左侧按钮功能制作 8.自动播放功能 在网页中,我们经常会看到各种轮播图的效果,它们到底是怎样实现的呢?今天,我们就一起来看一下!首先,我们需要准备若干张图片,在这里我准备了五张图片. 功能需求: 鼠标经过轮播图模块,左右按钮显示,离开隐藏左右按钮. 点击右侧按钮一次,图片往左播放

随机推荐