基于Pixi实现的Flappy Bird(2)--游戏准备与小鸟实现

1,652 阅读6分钟
  • 在准备好背景和地面之后,接下来就可以实现游戏的准备场景,还有控制小鸟的飞行

准备场景

  • 准备场景由一个 Get Ready! 文字图片和 TAP 提示图片组成
  • 创建一个 ReadyScene.js 文件,实现 ReadyScene 类,由于其只作显示,所以只需继承容器 Container 类,最后把这两个精灵对象添加到容器,并将整个容器放置在适当的位置即可
  • 其所需的纹理是在 Assets 类中预加载好的,需要在主模块中传进来
 import { Container, Sprite } from "pixi.js";
 class ReadyScene extends Container {
   constructor(options) {
     super();
     this.tip = options.texture.tip;
     this.tap = options.texture.tap;
     this.gameWidth = options.width; // 需要得知游戏的宽度
 ​
     this.visible = true; // 控制场景的可见性
     this.initReadyScene(); // 初始化准备场景
   }
 }
  • 声明 initReadyScene,用于初始化准备场景的两个精灵
 // 加载场景显示的元素
 initReadyScene() {
   this.initSceneEle(this.tip, 400);
   this.initSceneEle(this.tap, 530);
 }
 ​
 initSceneEle(texture, h) {
   const sprite = new Sprite(texture);
   sprite.scale.set(0.7, 0.7);
   sprite.position.set((this.gameWidth - sprite.width) / 2, h);
   this.addChild(sprite);
 }
  • 最后完善 Game 类中的 initScene 方法,将初始化场景实例化并添加到容器中,最后在舞台上就可以看到
 // 初始化游戏准备场景
 initScene() {
   this.readyScene = new ReadyScene({
     width: this.width,
     texture: this.assets.ready,
   });
   this.stage.addChild(this.readyScene); 
 }

image-20230316162916840.png

创建小鸟

  • 小鸟是游戏的主角,并且小鸟和其他显示对象不同,它会扇动翅膀飞行,因此需要使用动画精灵类 AnimatedSprite 来创建小鸟

    • AnimatedSprite 类需要一个纹理数组作为基础纹理,而这个纹理数组就是精灵动画的每一帧
  • 创建Bird.js 文件,实现一个 Bird 类,继承自 AnimatedSprite,小鸟需要一些初始状态,比如开始的 xy 坐标飞行起始时间扇动翅膀的频率

  • 由于小鸟在准备状态和飞行状态下,会以嘴巴附近作为基准点旋转,所以一开始还需要设置小鸟的基准点

 import { AnimatedSprite } from "pixi.js";
 ​
 class Bird extends AnimatedSprite {
   constructor(options) {
     super(options.texture);
     this.gameHeight = options.height; // 记录游戏的高度
     this.groundY = options.groundY; // 存储地面的y坐标
     
     // 设置小鸟基准点
     this.anchor.set(1, 0.5);
     // 设置小鸟的大小
     this.scale.set(0.8, 0.8);
     // 设置小鸟的位置
     this.position.set(0, (this.gameHeight - this.height) / 2);
     
     this.startX = 150; // 小鸟的起始x坐标
     this.startY = (this.gameHeight - this.height) / 2; // 小鸟的起始y坐标
     this.flyStartTime = 0; // 小鸟飞行起始时间
     this.animationSpeed = 0.2; // 设置扇动翅膀的频率
     
     this.isDead = true; // 小鸟是否已死亡
     this.isUp = false; // 小鸟是否正在上升
 }
  • 在游戏中小鸟一共有以下三个状态:

    1. 准备状态: 此时小鸟没有飞行,只是轻微的上下漂浮
    2. 开始飞行: 每次点击画面时,小鸟就会往上飞一段距离然后下坠
    3. 飞行过程: 在整个飞行过程中,只需要控制小鸟的姿态和 y 轴坐标(x 轴不变)

准备状态

  • 先声明准备状态的 birdReady 方法,该方法中设置小鸟的 xy 的位置,并重置小鸟的 rotation0

  • 调用 AnimatedSprite 上的 play() 方法,让小鸟煽动翅膀

  • 最后使用 pixipixi-ease 缓动库,让小鸟上下漂浮

    • 安装 pixi-ease npm run pixi-ease
 import { Ease } from "pixi-ease";
 ​
 // 初始化小鸟的准备状态
 birdReady() {
   //设置起始坐标
   this.x = this.startX;
   this.y = this.startY;
   this.rotation = 0; // 旋转角度重置
   
   this.play(); // 小鸟扇动翅膀
   
   // 让小鸟上下浮动
   this.ease = new Ease();
   this.ease.add(
     this,
     { rotation: -0.15, y: this.y + 4 }, 
     { duration: 400, reverse: true, repeat: true }
   );
 }
  • 接着在 Game.js 中,实例化 Bird
 // 初始化小鸟
 initBird() {
   this.bird = new Bird({
     height: this.height,
     groundY: this.ground.y,
     texture: this.assets.bird,
   });
   this.bird.zIndex = 1; // 设置小鸟的层级
   this.stage.addChild(this.bird);
 }
  • 然后再 Game.js 声明一个 gameReady 方法,初始化游戏的准备状态
 // 游戏准备状态
 gameReady() {
   // 准备场景可见
   this.readyScene.visible = true;
   // 设置游戏状态为ready
   this.state = "ready";
   // 初始化小鸟为准备状态
   this.bird.birdReady();
 }
  • gameReady 方法在 createGame 中调用之后,游戏的准备状态就会呈现

20230317141529.gif

开始飞行

  • 当玩家点击屏幕后,小鸟开始往上飞行,往上一段距离后,接着开始往下掉落,之后玩家每点击一次屏幕,小鸟都会重复这个过程

  • 这个过程就看作玩家点击一次,小鸟做一次竖直上抛运动,接着自由落体,这样就需要声明三个因素:

    1. 小鸟的每次往上飞的距离 FLY_HEIGHT
    2. 小鸟每次往上飞的初速度 INIT_VELOCITY
    3. 小鸟的重力加速度 GY
  • 于是在 Bird 类中,定义小鸟的三个因素

    • 重力加速度 GY10m/s²,在游戏中以毫秒为单位,所以要除以 1000 ,游戏中的重力加速度不需要和现实中这么大,最后取 3/10 就行
    • 每次点击屏幕往上飞的距离是 50
    • 知道加速度和飞行距离,那么根据速度位移公式 初速度² - 末速度² = 2 * 加速度 * 位移,竖直上抛 末速度0,这样可求得 ,初速度 = (2 * 加速度 * 位移)^1/2
 // 小鸟的重力加速度
 static GY = (10 / 1000) * 0.3;
 // 小鸟每次往上飞的距离
 static FLY_HEIGHT = 50;
 // 小鸟往上飞的初速度
 static INIT_VELOCITY = Math.sqrt(2 * Bird.FLY_HEIGHT * Bird.GY);
  • Bird.js 中声明 birdStartFly 方法,对小鸟飞行的状态进行初始化,然后使用 TickerEase 缓动,循环小鸟的飞行过程
 // 小鸟开始飞
 birdStartFly() {
   this.isDead = false; // 小鸟状态为未死亡
   this.animationSpeed = 0.4; // 飞行时翅膀扇快一点,提高动画帧速度
   this.flyStartY = this.y; // 记录小鸟起始的飞行位置
   this.flyStartTime = +new Date(); // 记录小鸟开始飞行时间
   this.ease.destroy(); // 销毁所有的缓动动画
 ​
   // 飞行循环
   Ticker.shared.add(() => this.birdFlying());
 }

飞行过程

  • 声明一个 birdFlying 方法,通过 Ticker 来不断执行该方法,进而实现小鸟的飞行过程
  • 该方法首先判断小鸟是否已经死亡,如果死亡则直接 return
  • 首先算出小鸟的飞行时间,然后可以通过时间不断更新,结合时间位移公式 位移 = 初速度 * 时间 - 1/2 * 加速度 * 时间²
  • 那么小鸟的起始飞行位置 flyStartY 与飞行距离 distance 的差值,就是小鸟当前所处的高度
  • 并且玩家每点击一次,开始飞行时间 flyStartTime 都会在 birdStartFly 方法中更新为当前时刻,那么飞行距离就会重新开始计算
 birdFlying() {
   if (this.isDead) return;
   // 飞行时间
   const time = +new Date() - this.flyStartTime;
   // 垂直飞行距离
   const distance = Bird.INIT_VELOCITY * time - 0.5 * Bird.GY * time * time;
   // 小鸟当前所处的高度
   const birdY = this.flyStartY - distance;
   // 判断小鸟是否调到地面了
   // ...
 }
  • 接着在 birdFlying 方法内判断,小鸟当前所处的高度 birdY 是否到达了地面

    • 若没有到达地面,则更新小鸟的 y 坐标,并且当小鸟的飞行距离大于0时,则小鸟此时往上飞,改变isUptrue,并让小鸟角度上仰;飞行距离小于0则是往下掉,改变isUpfalse,并让小鸟角度过渡为向下俯冲
    • 若小鸟到达了地面,则小鸟已经死亡,直接让小鸟的 y 坐标与地面相等,最后停止翅膀扇动和销毁缓动
  • birdFlying 方法中接着完善以下内容

 if (birdY <= this.groundY) {
   // 小鸟未落地
   this.y = birdY;
   if (distance > 0 && !this.isUp) {
     this.isUp = true;
     this.ease.add(this, { rotation: -0.3 }, { duration: 200 });
   } else if (distance < 0 && this.isUp) {
     this.isUp = false;
     this.ease.add(this, { rotation: 1.3 }, { duration: this.groundY - this.y });
   }
 } else {
   // 小鸟已经落地,即死亡
   this.y = this.groundY;
   this.isDead = true;  // 改变小鸟死亡状态
   this.animationSpeed = 0; // 动画速度为0
   this.stop(); // 销毁翅膀扇动动画
   this.ease.destroy(); // 销毁缓动动画
 }
  • 接下来可以给全局舞台 stage 绑定一个 click(浏览器)tap(手机模式),点玩家点击后开始游戏
 this.app.stage.interactive = true;
 this.app.stage.on("tap", this.onUserClick);
  • 声明 onUserClick,让游戏开启起来,并且点击一次调用小鸟飞行方法一次,小鸟就会不断重复做竖直上抛和自由落体运动
 onUserClick() {
   if (this.game.state === "over") return; // 如果游戏结束,直接return
   if (this.game.state !== "running") this.game.gameStart(); // 更改游戏为开启状态
   this.game.bird.birdStartFly();
 },
  • Game.js 中,更改游戏当前状态
 gameStart() {
   // 隐藏准备场景
   this.readyScene.visible = false;
   // 游戏状态为running
   this.state = "running";
 }
  • 至此,点击屏幕后,游戏开始,之后的每次点击屏幕,小鸟都会从当前位置重新开始做竖直上抛运动

20230317163910.gif