基于Pixi实现的Flappy Bird(4)--碰撞检测与得分

464 阅读6分钟
  • 目前障碍物已经可以持续从右往左移动,那么接下来就是计算小鸟穿过的障碍物数量,以及判断小鸟与障碍物是否碰撞

初始化分数

  • 由于分数的 0 到 9 也有各自对应的纹理,那么可以通过创建 Sprite 进行显示

image-20230323174937584.png

  • 这些分数精灵可以设置成一个精灵组合,放到一个装载容器内进行显示,最后将装载容器放置到舞台上
  • 创建 Score.js 文件,定义 Score 类,并让其继承于 Container
 import { Container, Sprite } from "pixi.js";
 class Score extends Container {
   constructor(options) {
     super();
     this.gameWidth = options.width;
     this.scoreTexture = options.texture;
     this.position.set(0, 170);
     this.scale.set(0.8, 0.8);
   }
 ​
 export default Score;
  • 这个分数是不断变化的,那么也就是说创建分数精灵的纹理需要在 0 到 9 之间不断变化
  • 声明一个 setData 方法,接收一个 score 作为参数,通过传入 score 的长度决定需要显示多少个分数精灵,并且在预加载资源时,创建分数的纹理组成了一个数组,通过数组下标就能获取到对应数字
 setData(score) {
   this.removeChildren(); // 首先把原来的精灵移除
   
   // 将传入的数字切割成数组,如传入12,切割后=>['1','2']
   const scoreArr = score.toString().split(""); 
   
   scoreArr.forEach((item, i) => {
     // 通过切割出的数字,获得对应的纹理并创建精灵
     const sprite = new Sprite(this.scoreTexture[item]);
     // 设置精灵的位置
     sprite.position.set(i * 65, 0);
     // 设置装载容器的位置
     this.x = (this.gameWidth - this.width) / 2 - i * 20;
     // 添加到容器内
     this.addChild(sprite);
   });
 }
  • Game.js 中,完善 initScore 方法,对分数进行实例化
 initScore() {
   this.score = new Score({
     width: this.width,
     texture: this.assets.score,
   });
   this.score.zIndex = 1;
   this.stage.addChild(this.score);
 }
  • Game.jsgameReady 方法中,调用 Score 类的 setData 方法,在游戏准备时初始化当前分数为0
 gameReady() {
   //...
   
   this.score.visible = true; // 分数可见
   this.curScore = 0; // 当前得分设置为0
   this.score.setData(this.curScore);
   
   //...
 }
  • 至此分数已经在屏幕中看到,并且测试改变当前分数 curScore ,看看分数的变化

image-20230323180936422.png

碰撞检测和得分

  • 得分,也就是计算出小鸟已经穿越过障碍物的数量

  • Pipe 类中定义 calcPassPipe 方法,传入一个参数 x 为小鸟的坐标,根据障碍物的宽度和已穿越的管子的数量,就可以统计出总的数量

    • 其实小鸟的 x 坐标是不变的,需要通过障碍物移动的相对位置去计算小鸟通过障碍物的数量
    • 障碍物的 x 坐标是持续变化的,用小鸟的 x 坐标减去障碍物容器的 x 坐标,记为 birdX
    • 由于障碍物是不断左移的,其 x 坐标持续变小的,当 birdX 大于 0 则表明小鸟已经开始穿越障碍物
    • birdX 大于 0 之后,障碍物往做移动,相当于小鸟往右移动,birdX 就是相对的一个 x 坐标
    • 然后用 birdX 除以障碍物宽度,就可以得到小鸟穿越的障碍物数量
    • 前面重置障碍物的方法里就有一个变量 passPipe,每次重置障碍物时,代表小鸟已经穿越了两个管子,此时计算得分时需要累加进去
 calcPassPipe(x) {
   let count = 0;
   const birdX = x - this.x; // 记录小鸟是否开始穿越障碍物
   if (birdX > 0) {
     // 计算穿越的障碍物数量,这里需要+0.5再向下取整,是因为障碍物宽度包括管子和间隔,小鸟只需完全穿越管子后就可以累加得分
     const num = Math.floor(birdX / Pipe.WIDTH + 0.5);
     count += num;
   }
   count += this.passPipe; // 这里额外加一次count,因为障碍物会重置
   return count; // 计算好的得分返回
 }

计算得分

  • 那么在 Game.js 中,声明一个 calcScore 方法,用于不断更新当前的得分
 calcScore() {
   const count = this.pipe.calcPassPipe(this.bird.x);
   this.curScore = count;
 }
  • Game.js 中的 gameStart 方法中使用 Ticker 开启游戏监听,不断更新当前得分
 gameStart() {
   //...
   // 开启游戏监听
   this.updateTicker = Ticker.shared;
   this.updateTicker.add(() => this.onUpdate());
 }
  • 声明 onUpdate 方法,用于不断更新当前得分,后面还要用于碰撞检测和判断小鸟是否死亡
 onUpdate() {
   if (this.state === "ready") return;
   this.calcScore(); // 先更新当前得分
   this.score.setData(this.curScore); // 改变显示的分数精灵
 }
  • 此时当用户点击屏幕时就能看到,随着障碍物的不断推移,得分会不断更新

20230324114243.gif

碰撞检测

  • 当小鸟飞行穿越障碍的时候,需要检测小鸟与障碍是否发生了碰撞,接下来封装一个碰撞检测的方法
  • 新建 bump.js 文件,这里因为精灵默认是矩形,所以封装一个矩形精灵检测碰撞的方法
  • 首先需要对精灵添加上碰撞检测需要的属性
 const addCollisionProperties = (sprite) => {
   // 全局x坐标属性
   if (!sprite.gx) {
     Object.defineProperty(sprite, "gx", {
       get() {
         return sprite.getGlobalPosition().x;
       },
       configureable: true,
       enumerable: true,
     });
   }
 ​
   // 全局y坐标属性
   if (!sprite.gy) {
     Object.defineProperty(sprite, "gy", {
       get() {
         return sprite.getGlobalPosition().y;
       },
       configureable: true,
       enumerable: true,
     });
   }
 ​
   // 精灵水平中心点属性
   if (!sprite.centerX) {
     Object.defineProperty(sprite, "centerX", {
       get() {
         return sprite.x + sprite.width / 2;
       },
       configureable: true,
       enumerable: true,
     });
   }
 ​
   // 精灵垂直中心点属性
   if (!sprite.centerY) {
     Object.defineProperty(sprite, "centerY", {
       get() {
         return sprite.y + sprite.height / 2;
       },
       configureable: true,
       enumerable: true,
     });
   }
 ​
   // 精灵半宽
   if (!sprite.halfWidth) {
     Object.defineProperty(sprite, "halfWidth", {
       get() {
         return sprite.width / 2;
       },
       configureable: true,
       enumerable: true,
     });
   }
 ​
   // 精灵半高
   if (!sprite.halfHeight) {
     Object.defineProperty(sprite, "halfHeight", {
       get() {
         return sprite.height / 2;
       },
       configureable: true,
       enumerable: true,
     });
   }
 ​
   // 水平锚点偏差
   if (!sprite.xAnchorOffset) {
     Object.defineProperty(sprite, "xAnchorOffset", {
       get() {
         return sprite.width * sprite.anchor?.x || 0;
       },
       configureable: true,
       enumerable: true,
     });
   }
 ​
   // 垂直锚点偏差
   if (!sprite.yAnchorOffset) {
     Object.defineProperty(sprite, "yAnchorOffset", {
       get() {
         return sprite.height * sprite.anchor?.y || 0;
       },
       configureable: true,
       enumerable: true,
     });
   }
 ​
   // 精灵半径
   if (sprite.circular && !sprite.radius) {
     Object.defineProperty(sprite, "radius", {
       get() {
         return sprite.width / 2;
       },
       enumerable: true,
       configurable: true,
     });
   }
 ​
   sprite._bumpPropertiesAdded = true; // 用于标记精灵是否已经添加以上属性
 };
  • 封装碰撞检测方法

    • 在这个游戏中,障碍物是装在自己的容器中,在判断碰撞时不能使用障碍物的局部坐标,而是使用其全局内的坐标去判断
    • 此外还要注意,小鸟的基准点是重新设置在嘴的位置,在判断距离时需要减去其基准点所带来的偏差

image-20230324122618345.png

 // 第三个参数global是否使用全局坐标
 const hitTestSprite = (s1, s2, global = false) => {
   // 添加检测碰撞的属性
   if (!s1._bumpPropertiesAdded) addCollisionProperties(s1);
   if (!s2._bumpPropertiesAdded) addCollisionProperties(s2);
 ​
   // 用于确定是否有碰撞的变量
   let hit = false;
   let s1X, s1Y, s2X, s2Y;
 ​
   // 计算精灵的相差距离
   if (global) {
     // 全局下精灵A中心点的位置
     s1X = s1.gx + Math.abs(s1.halfWidth) - s1.xAnchorOffset;
     s1Y = s1.gy + Math.abs(s1.halfHeight) - s1.yAnchorOffset;
     // 全局下精灵B的位置
     s2X = s2.gx + Math.abs(s2.halfWidth) - s2.xAnchorOffset;
     s2Y = s2.gy + Math.abs(s2.halfHeight) - s2.yAnchorOffset;
   } else {
     // 局部精灵A的中心点的位置
     s1X = s1.x + Math.abs(s1.halfWidth) - s1.xAnchorOffset;
     s1Y = s1.y + Math.abs(s1.halfHeight) - s1.yAnchorOffset;
     // 局部精灵B的中心点的位置
     s2X = s2.x + Math.abs(s2.halfWidth) - s2.xAnchorOffset;
     s2Y = s2.y + Math.abs(s2.halfHeight) - s2.yAnchorOffset;
   }
   const gapX = s1X - s2X; // 精灵水平相差的间距
   const gapY = s1Y - s2Y; // 精灵垂直相差的间距
 ​
   // 算出精灵半宽、半高和
   const combineWidth = Math.abs(s1.halfWidth) + Math.abs(s2.halfWidth);
   const combineHeight = Math.abs(s1.halfHeight) + Math.abs(s2.halfHeight);
 ​
   // 判断两个精灵相差距离,是否小于精灵半宽和
   // 先检查水平方向是否有碰撞
   if (Math.abs(gapX) < combineWidth) {
     // 检查垂直方向是否有碰撞
     hit = Math.abs(gapY) < combineHeight;
   } else {
     hit = false;
   }
   return hit;
 };
  • 方法封装之后,在 Pipe 类中引入并使用,用于检测障碍物和小鸟是否碰撞
  • 声明 checkCollision 方法,接收一个小鸟对象作为参数,使用封装的方法进行检测
 checkCollision(bird) {
   const len = this.children.length;
   for (let i = 0; i < len; i++) {
     // 判断每一根管子是否与小鸟有碰撞
     if (hitTestSprite(this.children[i], bird, true)) return true;
   }
   return false;
 }
  • 最后在 Game.jsonUpdate 方法中,进行持续的碰撞检测
 onUpdate() {
   const isHit = this.pipe.checkCollision(this.bird); // 持续碰撞检测
   if (this.state === "ready") return;
   if (this.bird.isDead) return this.gameOver(); // 如果小鸟状态为死亡,则游戏结束
   if (isHit) return this.gameOver(); // 如果撞了,则游戏结束
   this.calcScore();
   this.saveBestScore();
   this.score.setData(this.curScore);
 }
  • Game.js 中声明 gameOver 方法,用于确定游戏的结束状态

20230324142333.gif

  • 可以看到游戏结束需要实现以下内容

    • 设置游戏状态为 over
    • 隐藏顶部的分数
    • 障碍物停止移动
    • 销毁对游戏的持续监听
    • 显示结束场景
 gameOver() {
   if (this.state === "over") return;
   this.state = "over"; // 设置状态为over
   this.overScene.show(this.curScore, this.bestScore); // 显示结束场景
   this.score.visible = false; // 隐藏分数
   this.pipe.stopMove();// 调用pipe的stopMove方法,让停止障碍物移动
   this.updateTicker.destroy(); // 移除游戏监听
 }
  • 由于目前结束场景还未实现,只有停止障碍物移动和隐藏分数

20230324143925.gif

最高得分

  • Game.js 中声明 saveBestScore 方法,每次游戏结束都会从浏览器缓存中拿最好分数
  • 如果当前分数要比缓存中的大,则拿当前分数作为最大分数,并更新浏览器缓存中的分数
  • 此方法在 onUpdate 中会不断调用
 saveBestScore() {
   this.bestScore = window.localStorage.getItem("bestScore") || 0;
   if (this.curScore > this.bestScore) {
     this.bestScore = this.curScore;
     window.localStorage.setItem("bestScore", this.curScore);
   }
 }