phaser3之飞机大战(四)

2,279 阅读4分钟

接上篇phaser3之飞机大战(三)
本篇为飞机大战的结尾,包含内容:敌机发射子弹、飞机、敌机爆炸效果、积分、结束后重新开始场景、添加奖牌机制、添加音效。

项目线上链接: yupeng.pro/phaser3/pla…

写在前面

该飞机大战素材来源于phaser小站:
www.phaser-china.com/tutorial-de…
原文使用phaser2进行实现,本文用phaser3将飞机大战重构一遍,在实战中学习一下phaser3的api
源码:github.com/YUPENG12138…

一、敌机发射子弹

敌机发射子弹的逻辑和我方飞机发射子弹的逻辑基本相同,我们首先还是要创建一个敌机的子弹组,在敌机需要子弹的时候在组里取。

// 首先添加一个创建子弹的方法
function EnemyBulletClass(gameHeight) {
  return new Phaser.Class({
    Extends: Phaser.GameObjects.Sprite,
    initialize: function Bullet(scene) {
      Phaser.GameObjects.Sprite.call(this, scene, 0, 0, 'bullet');
    },
    update: function () {
      if (this.y > gameHeight) {
        this.hide();
      }
    },
    fire: function () {
      this.setActive(true);
      this.setVisible(true);
    },
    hide: function() {
      this.setActive(false);
      this.setVisible(false);
    }
  });
}

// 然后在play场景create生命周期里创建子弹组
// 创建敌机子弹组
const EnemyBullteClass = EnemyBulletClass(this.game.config.height);
this.enemyBullets = this.add.group({
  classType: EnemyBullteClass,
  runChildUpdate: true,
});

敌机子弹组创建好了,接下来就是需要处理敌机发射子弹的逻辑

 // 创建敌机组
['enemy1', 'enemy2', 'enemy3'].forEach((item) => {

  // 在创建敌机的方法里 我们新增一个当前子弹组的参数`this.enemyBullets`	
  const EnemyClass = EnemyFactory(item, this.game.config.height, this.enemyBullets);
  this[item] = this.add.group({
    classType: EnemyClass,
    runChildUpdate: true,
  });
});

// 然后我们在EnemyFactory中取接收参数,并处理发射子弹逻辑。
function EnemyFactory(key , gameHeight, enemyBullets) {
  return new Phaser.Class({
    Extends: Phaser.GameObjects.Sprite,
    initialize: function Bullet(scene) {
    
      Phaser.GameObjects.Sprite.call(this, scene, 0, 0, key);
      
      // 初始化这里我们给enemybuttle加几个属性,bulletSpeed子弹速度,life敌机生命,enemyButtelBeforeTime上一个子弹发射的时间,index不同飞机的标识key
      // 这里我们根据飞机大小设定,飞机越大发射子弹的时间间隔越小
      this.bulletSpeed = 1000 * (4 - key.replace('enemy', ''));
      this.life = key.replace('enemy', '');
      this.enemyButtelBeforeTime = 0;
      this.index = key.replace('enemy', '');
    },
    update: function () {
    
      // 在update里, 我们同样拿一下当前的时间与上次的时间做一下比对
      const time = new Date().getTime();
      if (this.y > gameHeight) {
        this.hide();
      }
      
      // 当时间捡个超过发射速度的时候
      if (time - this.enemyButtelBeforeTime >= this.bulletSpeed) {
      
        // 我们在敌机子弹组里取一个子弹出来 这里的逻辑和飞机发射子弹基本一致
        const bullet = enemyBullets.getFirstDead(true);
        if (bullet) {
          bullet.fire();
          bullet.setPosition(this.x, this.y + this.height / 2);
          this.scene.physics.add.existing(bullet);
          
          // 这里我们设置子弹的飞行速度,飞机越大,子弹的飞行速度越慢
          bullet.body.setVelocity(0, 100 * (4 - key.replace('enemy', '')));
          this.enemyButtelBeforeTime = time;
        }
      }
    },
    show: function () {
      this.setActive(true);
      this.setVisible(true);
    },
    hide: function() {
      this.setActive(false);
      this.setVisible(false);
    }
  });
}

接下来我们添加一下碰撞检测,包括我方子弹和敌方子弹碰撞,我方飞机和敌方子弹碰撞

// play场景create生命周期里 
this.physics.add.overlap(this.bullets, this.enemyBullets, function (bullet, enemyBullet) {
  bullet.destroy();
  enemyBullet.destroy();
}, null, this);
this.physics.add.overlap(this.enemyBullets, this.plane, function (enemyBullet, plane) {
  plane.destroy();
  this.gameOver = true;
}, null, this);
// 一些其他的处理
// 引入我的子弹 这里的判断加了this.gameOver 在游戏结束的时,我方飞机就不再发射子弹
if (time - this.myBulletBeforeTime > 200 && !this.gameOver) {
  const bullet = this.bullets && this.bullets.getFirstDead(true);
  ...
}

 // 引入敌机 这里我修改了下敌机引入的速度
if (time - this.enemyBeforeTime > 800) {
  const enemyIndex = Phaser.Math.Between(1, 3);
  const enemy = this[`enemy${enemyIndex}`].getFirstDead(true);
  if (enemy) {
    enemy.show();
    enemy.setOrigin(0.5, 0.5);
    enemy.setPosition(Phaser.Math.Between(0 + enemy.width, this.game.config.width - enemy.width), 0);
    this.physics.add.existing(enemy);
    enemy.body.setVelocity(0, 200);
    
    // 敌机飞行的速度也根据不同的飞机做了限制,越大非得越慢
    enemy.body.setVelocity(0, 50 * (4 - enemyIndex));
    this.enemyBeforeTime = time;
  }
}

敌机发射子弹的效果就做好了,我们看一下效果:

二、我方飞机、敌机爆炸效果

爆炸的效果其实就是一个帧动画效果,且我们不需要这个动画效果有什么移动,碰撞检测之类的原有敌机的逻辑,所以可以在敌机消失的时候,再引入一个精灵专门用来展示帧动画效果,然后在展示完效果之后消失就可以了

// 首先还是先把需要的帧图片引入进来
 if (key === 'explode1') {
  this.load.spritesheet(key, require(`./${assetsMap[key]}`), {
    frameWidth: 20,
    frameHeight: 20,
  });
  continue;
}
if (key === 'explode2') {
  this.load.spritesheet(key, require(`./${assetsMap[key]}`), {
    frameWidth: 30,
    frameHeight: 30,
  });
  continue;
}
if (key === 'explode3') {
  this.load.spritesheet(key, require(`./${assetsMap[key]}`), {
    frameWidth: 50,
    frameHeight: 50,
  });
  continue;
}
if (key === 'myexplode') {
  this.load.spritesheet(key, require(`./${assetsMap[key]}`), {
    frameWidth: 40,
    frameHeight: 40,
  });
  continue;
}

接下来创建飞机爆炸帧动画

// 创建我方飞机爆炸帧动画,我方飞机爆炸循环一次  速度慢点
this.anims.create({
  key: 'planeBoom',
  frames: this.anims.generateFrameNumbers('myexplode', {
    start: 0,
    end: 3
  }),
  frameRate: 2,
  repeat: 1
});

// 创建敌机组
['enemy1', 'enemy2', 'enemy3'].forEach((item) => {
  const EnemyClass = EnemyFactory(item, this.game.config.height, this.enemyBullets);
  this[item] = this.add.group({
    classType: EnemyClass,
    runChildUpdate: true,
  });

  const key = item.replace('enemy', '');
  // 创建敌机爆炸帧动画 敌方飞机爆炸不循环 速度快点
  this.anims.create({
    key: `enemyBoom${key}`,
    frames: this.anims.generateFrameNumbers(`explode${key}`, {
      start: 0,
      end: 2
    }),
    frameRate: 5,
    repeat: 0
  });
});

下面我们处理飞机爆炸的调用时机
我方飞机在与

// 敌方飞机在与我方子弹碰撞时进行爆炸,在生成敌机时我们给飞机挂载了life生命字段,大飞机3点 小飞机1点
['enemy1', 'enemy2', 'enemy3'].forEach((item) => {
  this.physics.add.overlap(this.bullets, this[item], function (bullet, enemy) {
    bullet.destroy();
    
    // 检测碰撞后先减生命,发现不够了再让敌机消失,调用爆炸帧动画
    enemy.life = enemy.life - 1;
    if (enemy.life <= 0) {
      enemy.destroy();
      const key = item.replace('enemy', '');
      
      // 我们在敌机消失的位置上新增加一个精灵,用来展示帧动画
      const enemyFrame = this.add.sprite(enemy.x, enemy.y, `explode${key}`);
      enemyFrame.anims.play(`enemyBoom${key}`);
      
      // 这里遇到一个新的api,用来监听动画执行结束
      // api: https://photonstorm.github.io/phaser3-docs/Phaser.Animations.Events.html#event:SPRITE_ANIMATION_COMPLETE
      // once 表示只执行一次
      // api: https://photonstorm.github.io/phaser3-docs/Phaser.Events.EventEmitter.html#once
      enemyFrame.once('animationcomplete', function() {
        enemyFrame.destroy();
      })
    }
  }, null, this);
});

// 我方飞机爆炸效果
this.physics.add.overlap(this.enemyBullets, this.plane, function (enemyBullet, plane) {
  plane.destroy();
  this.gameOver = true;
  
  // 在与敌方子弹碰撞时展示
  const myPlaneFrame = this.add.sprite(plane.x, plane.y, 'myexplode');
  myPlaneFrame.anims.play('planeBoom');
  myPlaneFrame.once('animationcomplete', function() {
    myPlaneFrame.destroy();
  })
}, null, this);

我们来看一下爆炸和敌机生命效果

三、积分

在敌机被击毁时,我们添加分数

// 添加文本 将原来生成的文本存起来,再存一个分数字段
this.scoreText = this.add.text(0, 0, 'Score: 0', { color: '#ff0000', fontSize: '16px' });
this.score = 0;

...

// 在检测敌机碰撞里设置
this.score = +key + this.score;
this.scoreText.setText(`Score: ${this.score}`, this.score);

四、结束之后重新开始场景

这个场景基本和开始场景一致,多一个展示积分的栏位。 代码上的修改可以看这个commit,我就不细说了。 结束之后重新开始场景

主要的逻辑就是在我方飞机爆炸完之后,跳转场景这里有一块要注意,就是怎么在另一个场景拿到上一个场景的数据

// start方法接收第二个参数,可以给下个场景传参 这里要注意,如果只是传一个值,这个值为0的时候,会默认接收一个`{}`,所以我们传值还是以对象的方式去传
this.scene.start('restart', {
  score: this.score,
});

// 接收的场景在create生命周期函数里接收传值
create(data) {
...
}

我们来看一下效果:

五、添加奖牌机制

现在飞机大战基本就完成了,但我方飞机只有一点生命,一次只能发射一个子弹,我们可以加入奖牌机制,丰富游戏的玩法。

// 我们还是先创建个生成奖牌的方法
const AwardClass = new Phaser.Class({
  Extends: Phaser.GameObjects.Sprite,
  initialize: function Award(scene) {
    Phaser.GameObjects.Sprite.call(this, scene, 0, 0, 'award');
  },
  update: function () {
    if (this.y < this.scene.game.height) {
      this.hide();
    }
  },
  show: function () {
    this.setActive(true);
    this.setVisible(true);
    this.setOrigin(0.5, 0.5);
    console.log(this);
    this.setPosition(Phaser.Math.Between(0 + this.width, this.scene.game.config.width - this.width), 0);
    this.scene.physics.add.existing(this);
    this.body.setVelocity(0, 100);
  },
  hide: function() {
    this.setActive(false);
    this.setVisible(false);
  }
});

...

// 在play场景create中处理

// 引入飞机精灵 默认给生命为1
this.plane = this.add.sprite(this.game.config.width / 2, 100, 'myplane').setInteractive({ draggable: true });
this.plane.life = 1;


// 然后创建奖牌组
this.awards = this.add.group({
  classType: AwardClass,
  runChildUpdate: true,
});

// 在检测飞机与奖牌碰撞,生命+1 最多不超过3
this.physics.add.overlap(this.plane, this.awards, function (plane, award) {
  award.destroy();
  if (plane.life < 3) {
    plane.life += 1;
  }
}, null, this);

// 创建奖牌 每10秒创建一个
// 这里涉及到phaser的时间管理器,我们可以用这个方法去设定每10秒生成一个奖牌,为了方便演示 我们设置成3秒
// api: https://photonstorm.github.io/phaser3-docs/Phaser.Time.Clock.html#addEvent__anchor
this.time.addEvent({
  loop: true,
  delay: 3000,
  callback: () => {
    const award = this.awards.getFirstDead(true);
    award.show();
  }
});

// 飞机碰到敌方子弹时 生命-1
this.physics.add.overlap(this.enemyBullets, this.plane, (enemyBullet, plane) => {
  enemyBullet.destroy();
  if (plane.life > 1) {
    plane.life -= 1;
    return;
  }
  ...
}

我们在吃到奖牌的时候不光要加生命,在子弹上也要有些变化,两条生命一次发射3个子弹,三条生命一次发射5个子弹

// 我们把update中生成子弹的逻辑抽出来
// 引入我的子弹
if (time - this.myBulletBeforeTime > 200 && !this.gameOver) {
  /*
  const bullet = this.bullets && this.bullets.getFirstDead(true);
  if (bullet) {
    bullet.fire();
    bullet.setPosition(this.plane.x, this.plane.y - this.plane.height / 2);
    this.physics.add.existing(bullet);
    bullet.body.setVelocity(0, -300);
    this.myBulletBeforeTime = time;
  }
  */
  createBulletByLife.call(this);
  this.myBulletBeforeTime = time;
 }
 
// 新增一个通过生命数量创建子弹的方法
function createBulletByLife() {
  if (this.plane.life === 2) {
    for (let index = 0; index < 3; index++) {
      createBullets.call(this, index - 1);
    }
  } else if (this.plane.life === 3) {
    for (let index = 0; index < 5; index++) {
      createBullets.call(this, index - 2);
    }
  } else if (this.plane.life === 1) {
    createBullets.call(this, 0);
  }

  function createBullets(index) {
    const bullet = this.bullets && this.bullets.getFirstDead(true);
    if (bullet) {
      bullet.fire();
      bullet.setPosition(this.plane.x, this.plane.y - this.plane.height / 2);
      this.physics.add.existing(bullet);
      bullet.body.setVelocity(index * 30, -300);
    }
  }
}

看一下奖牌效果:

六、添加音效

这边介绍一下用到的音效相关的api,项目实现具体可以看commit添加音效

// 首先是音效的引入
// api: https://photonstorm.github.io/phaser3-docs/Phaser.Loader.LoaderPlugin.html#audio__anchor
this.load.audio(key, require(`./${assetsMap[key]}`));

// 添加声音 根据导入的key生成添加音效
// api: https://photonstorm.github.io/phaser3-docs/Phaser.Sound.BaseSoundManager.html#add__anchor
const startAudio = this.sound.add('normalback');

// 音效的播放
// api: https://photonstorm.github.io/phaser3-docs/Phaser.Sound.BaseSound.html#play__anchor
startAudio.play({
  loop: true,
  volume: 0.1,
})

// 音效停止
// api: https://photonstorm.github.io/phaser3-docs/Phaser.Sound.BaseSound.html#stop__anchor
startAudio.stop();

那么phaser3飞机大战demo就到这里完成了,接下来会分享游戏2048。