D31 - 「来互相伤害啊!」:无聊我要见到血流成河

上竞技场就是要决斗阿,不然要干嘛。

来让人物发射武器!血流成河吧!

首先来回顾一下 D27 武器规划。

D26 - 规划游戏场景:主场景.png

  • 主角武器
    • 会与敌人发生碰撞
    • 向下飞行、随机旋转
    • 最多只能存在 1 个武器,不能连续发射
  • 敌人武器
    • 会与主角发生碰撞
    • 向上飞行、随机旋转
    • 最多只能存在 5 个武器,不能连续发射

可以发现同一时间内会出现多个相同的武器,所以这时候要出动 Phaser 的 Group。

Group 可以用来管理重复出现的物体,更容易进行侦测与各类操作,详细说明可以参考以下连结。

Docs - Phaser.Physics.Arcade.Group

可以发现不管是主角武器还是敌人武器,几乎所有性质都相同,所以我们可以先建立 group-weapon.js 做为武器容器,建立角色武器时再将对应的武器注入、建立即可。

src\components\window-app-cat-vs-dog\objects\group-weapon.js

import Phaser from 'phaser';

/**
 * @typedef {Object} ConstructorParams
 * @property {Object} classType 注入武器物件
 * @property {string} key
 * @property {number} quantity 物体数量
 */

export default class extends Phaser.Physics.Arcade.Group {
  /**
   * @param {Phaser.Scene} scene 
   * @param {ConstructorParams} params 
   */
  constructor(scene, params) {
    super(scene.physics.world, scene);

    const { classType, key, quantity = 5 } = params;

    this.createMultiple({
      classType,
      frameQuantity: quantity,
      active: false,
      visible: false,
      key,
    });

    this.setDepth(1);

    // 隐藏所有武器
    this.getChildren().forEach((item) => {
      item.setScale(0);
    });
  }

  /** 发射武器
   * @param {number} x 
   * @param {number} y 
   * @param {number} velocity 
   */
  fire(x, y, velocity) {
    const weapon = this.getFirstDead(false);
    if (weapon) {
      weapon.body.enable = true;
      weapon.fire(x, y, velocity);
    }
  }
}

丢你鱼骨

首先建立主角的武器。

src\components\window-app-cat-vs-dog\objects\sprite-weapon-cat.js

import Phaser from 'phaser';

/**
 * @typedef {Object} ConstructorParams
 * @property {number} [x]
 * @property {number} [y]
 */

export default class extends Phaser.Physics.Arcade.Sprite {
  /**
   * @param {Phaser.Scene} scene 
   * @param {ConstructorParams} params
   */
  constructor(scene, params) {
    const { x = 0, y = 0 } = params;

    super(scene, x, y, 'cat-weapon');
    this.scene = scene;
  }

  preUpdate(time, delta) {
    super.preUpdate(time, delta);

    /** 检查武器是否超出世界边界
     * 透过侦测武器是否与世界有碰撞,取反向逻辑
     * 没有碰撞,表示物体已经超出边界
     */
    const outOfBoundary = !Phaser.Geom.Rectangle.Overlaps(
      this.scene.physics.world.bounds,
      this.getBounds(),
    );

    // 隐藏超出边界武器并关闭活动
    if (outOfBoundary) {
      this.setActive(false)
        .setVisible(false);
    }
  }

  /** 发射武器
   * @param {number} x 
   * @param {number} y 
   * @param {number} velocity 
   */
  fire(x, y, velocity) {
    // 清除所有加速度、速度并设置於指定座标
    this.body.reset(x, y);

    // 角速度
    const angularVelocity = Phaser.Math.Between(-400, 400);

    this.setScale(0.3)
      .setSize(160, 160)
      .setAngularVelocity(angularVelocity)
      .setVelocityY(velocity)
      .setActive(true)
      .setVisible(true);
  }
}

回到 scene-main.js 实际建立主角武器。

src\components\window-app-cat-vs-dog\scenes\scene-main.js

import Phaser from 'phaser';

import SpriteCat from '@/components/window-app-cat-vs-dog/objects/sprite-cat';
import SpriteDog from '@/components/window-app-cat-vs-dog/objects/sprite-dog';

import GroupWeapon from '@/components/window-app-cat-vs-dog/objects/group-weapon';
import SpriteWeaponCat from '@/components/window-app-cat-vs-dog/objects/sprite-weapon-cat';

export default class extends Phaser.Scene {
  constructor() {
    super({ key: 'main' })
  }
  create() {
    // 主角
    const catWeapon = new GroupWeapon(this, {
      classType: SpriteWeaponCat,
      key: 'cat-weapon',
      quantity: 1
    });

    this.cat = new SpriteCat(this);

    // ...
  }
}

建立主角武器後,利用相同概念,将武器注入主角物件中,前往 sprite-cat.js 加入以下内容。

  • 新增 weapon 变数,储存注入之武器
  • constructorparams 参数加入 weapon
  • joyStick 新增 on('rising') 监听(按钮按下事件),用来呼叫 weapon.fire()

src\components\window-app-cat-vs-dog\objects\sprite-cat.js

/**
 * @typedef {import('@/script/electronic-components/joy-stick').default} JoyStick
 * @typedef {import('@/components/window-app-cat-vs-dog/objects/group-weapon')} GroupWeapon
 * 
 * @typedef {Object} CatParams
 * @property {number} [x]
 * @property {number} [y]
 * @property {GroupWeapon} weapon
 */

import Phaser from 'phaser';

/** 最大速度 */
const velocityMax = 300;

export default class extends Phaser.Physics.Arcade.Sprite {
  weapon = null;

  /** 血量 */
  health = 5;

  /**
   * @param {Phaser.Scene} scene 
   * @param {CatParams} params
   */
  constructor(scene, params = {}) {
    const {
      x = 200, y = 200,
      weapon = null,
    } = params;

    if (!weapon) throw new Error('weapon 为必填参数');

    // ...

    this.scene = scene;
    this.weapon = weapon;

    /** @type {JoyStick} */
    const joyStick = scene.game.joyStick;

    joyStick.on('data', ({ x, y }) => {
      // ...
    }).on('rising', () => {
      // 座标设为与主角相同位置
      this.weapon.fire(this.x, this.y, 800);

      // 播放主角发射动画
      this.play('cat-attack', true);
      this.setVelocity(0, 0);
    });

    // ...
  }

  // ...
}

回到 scene-main.js,将武器武器注入主角中。

src\components\window-app-cat-vs-dog\scenes\scene-main.js

import Phaser from 'phaser';

import SpriteCat from '@/components/window-app-cat-vs-dog/objects/sprite-cat';
import SpriteDog from '@/components/window-app-cat-vs-dog/objects/sprite-dog';

import GroupWeapon from '@/components/window-app-cat-vs-dog/objects/group-weapon';
import SpriteWeaponCat from '@/components/window-app-cat-vs-dog/objects/sprite-weapon-cat';

export default class extends Phaser.Scene {
  constructor() {
    super({ key: 'main' })
  }
  create() {
    // 主角
    const catWeapon = new GroupWeapon(this, {
      classType: SpriteWeaponCat,
      key: 'cat-weapon',
      quantity: 1
    });
    this.cat = new SpriteCat(this, {
      weapon: catWeapon,
    });

    // ...
  }
}

试试看按下摇杆按钮有没有成功发射武器吧。

D31 - 主角发射武器.gif

成功发射!但是主角停留在发射动画,回到 sprite-cat.js 调整一下。

src\components\window-app-cat-vs-dog\objects\sprite-cat.js

// ...

export default class extends Phaser.Physics.Arcade.Sprite {
  weapon = null;

  /** 血量 */
  health = 5;

  /**
   * @param {Phaser.Scene} scene 
   * @param {CatParams} params
   */
  constructor(scene, params = {}) {
    // ...
  }

  preUpdate(time, delta) {
    super.preUpdate(time, delta);

    // 没有任何动画播放时,播放 cat-work
    if (!this.anims.isPlaying) {
      this.play('cat-work');
    }
  }

  // ...
}

D31 - 完成主角发射武器动画.gif

完成主角发射动画!

吃我骨头

接着来让狗狗喷骨头吧。

首先建立敌人武器 sprite-weapon-dog.js

import Phaser from 'phaser';

export default class extends Phaser.Physics.Arcade.Sprite {
  /**
   * @param {Phaser.Scene} scene 
   * @param {number} x 
   * @param {number} y 
   */
  constructor(scene, x = 0, y = 0) {
    super(scene, x, y, 'dog-weapon');
    this.scene = scene;
  }

  preUpdate(time, delta) {
    super.preUpdate(time, delta);

    const outOfBoundary = !Phaser.Geom.Rectangle.Overlaps(
      this.scene.physics.world.bounds,
      this.getBounds(),
    );

    if (outOfBoundary) {
      this.setActive(false);
      this.setVisible(false);
    }
  }

  /** 发射武器
   * @param {number} x 
   * @param {number} y 
   * @param {number} velocity 
   */
  fire(x, y, velocity) {
    this.body.reset(x, y);

    const angularVelocity = Phaser.Math.Between(-400, 400);

    this.setScale(0.2)
      .setSize(300, 300)
      .setAngularVelocity(angularVelocity)
      .setVelocityY(velocity)
      .setActive(true)
      .setVisible(true);
  }
}

scene-main.js 建立敌人武器并注入敌人物件中。

src\components\window-app-cat-vs-dog\scenes\scene-main.js

// ...

import GroupWeapon from '@/components/window-app-cat-vs-dog/objects/group-weapon';
import SpriteWeaponCat from '@/components/window-app-cat-vs-dog/objects/sprite-weapon-cat';
import SpriteWeaponDog from '@/components/window-app-cat-vs-dog/objects/sprite-weapon-dog';

export default class extends Phaser.Scene {
  constructor() {
    super({ key: 'main' })
  }
  create() {
    // 主角
    // ...

    // 敌人
    const dogWeapon = new GroupWeapon(this, {
      classType: SpriteWeaponDog,
      key: 'dog-weapon',
    });
    this.dog = new SpriteDog(this, {
      weapon: dogWeapon,
      target: this.cat,
    });

    // 加入中央河流
    // ...
  }
}

现在敌人也可以发射武器了,让我们回到 sprite-dog.js 中,让狗狗发射骨头吧!

src\components\window-app-cat-vs-dog\objects\sprite-dog.js

/**
 * @typedef {import('@/components/window-app-cat-vs-dog/objects/group-weapon')} GroupWeapon
 * 
 * @typedef {Object} DogParams
 * @property {number} [x]
 * @property {number} [y]
 * @property {Phaser.Physics.Arcade.Sprite} target
 * @property {GroupWeapon} weapon
 */

// ...

export default class extends Phaser.Physics.Arcade.Sprite {
  /** @type {Phaser.Physics.Arcade.Sprite} */
  target = null;

  /** @type {GroupWeapon} */
  weapon = null;
  health = 10;

  /**
   * @param {Phaser.Scene} scene 
   * @param {DogParams} params
   */
  constructor(scene, params) {
    const {
      x = 500, y = 600,
      weapon = null,
      target = null,
    } = params;

    if (!weapon) throw new Error('weapon 为必填参数');

    // ...

    this.scene = scene;
    this.target = target;
    this.weapon = weapon;

    this.initAutomata();
  }

  preUpdate(time, delta) {
    super.preUpdate(time, delta);

    if (!this.anims.isPlaying) {
      this.play('dog-work');
    }
  }

  // ...

  initAutomata() {
    // 随机发射
    this.scene.time.addEvent({
      delay: 500,
      callbackScope: this,
      repeat: -1,
      callback: async () => {
        await delay(Phaser.Math.Between(0, 200));
        this.fire();
      },
    });

    // 追猫
    // ...
  }

  fire() {
    this.weapon.fire(this.x, this.y, -500);
    this.play('dog-attack', true);
  }
}

D31 - 敌人发射武器.gif

可以看到狗狗开始很凶残得丢骨头了! ⎝(・ω´・⎝)

互相伤害吧!

鳕鱼:「再来就是人物与武器的激❤烈❤碰撞了!」

电子助教:「就不能用正常一点的方式描述碰撞侦测嘛 ...(´● ω ●`)」

加入人物扣血与胜败部分,先将人物的血量显示出来吧。

src\components\window-app-cat-vs-dog\scenes\scene-main.js

// ...

export default class extends Phaser.Scene {
  constructor() {
    super({ key: 'main' })
  }
  create() {
    // ...

    // 显示生命值
    this.catHealthText = this.add.text(20, 20, `猫命:${this.cat.health}`, {
      fill: '#000',
      fontSize: 14,
    });

    const sceneHeight = this.game.config.height;
    this.dogHealthText = this.add.text(20, sceneHeight - 20, `狗血:${this.dog.health}`, {
      fill: '#000',
      fontSize: 14,
    }).setOrigin(0, 1);

    // 加入中央河流
    // ...
  }
  update() {
    this.catHealthText.setText(`猫命:${this.cat.health}`);
    this.dogHealthText.setText(`狗血:${this.dog.health}`);
  }
}

Untitled

可以看到画面上多了猫命与狗血。

最後就是加入人物与武器的碰撞侦测了。

src\components\window-app-cat-vs-dog\scenes\scene-main.js

// ...

export default class extends Phaser.Scene {
  constructor() {
    super({ key: 'main' })
  }
  create() {
    // ...

    // 加入武器与人物碰撞
    this.physics.add.overlap(this.cat, dogWeapon, (cat, weapon) => {
      // 隐藏武器
      weapon.body.enable = false;
      weapon.setActive(false).setVisible(false);

      // 主角扣血
      this.cat.subHealth();
    });

    this.physics.add.overlap(this.dog, catWeapon, (dog, weapon) => {
      // 隐藏武器
      weapon.body.enable = false;
      weapon.setActive(false).setVisible(false);

      // 敌人扣血
      this.dog.subHealth();
    });
  }
  // ...
}

D31 - 人物与武器碰撞.gif

可以看到主角与敌人被击中时都会播放被击中动画,同时减少血量。

但是目前血量归零後,不会有任何变化,所以要怎麽进到结束场景呢?

很简单,由於我们已经在 sprite-cat.jssprite-cat.js 中加入「血量归 0 时,触发 death 事件的程序」。

所以最後只要监听人物的 death 事件即可。

src\components\window-app-cat-vs-dog\scenes\scene-main.js

// ...

export default class extends Phaser.Scene {
  constructor() {
    super({ key: 'main' })
  }
  create() {
    // ...

    // 侦测人物事件
    this.dog.once('death', () => {
      this.scene.start('over', 'win');
    });
    this.cat.once('death', () => {
      this.scene.start('over', 'lose');
    });
  }
  // ...
}

猫死表示失败,狗死表示游戏获胜。

将结果透过第二个参数传输到下一个场景,就可以在结束场景判断输赢了。

结束场景

最後我们将结束场景完成吧。

结束场景的程序非常简单,就是根据传来的资料显示对应的结果。

src\components\window-app-cat-vs-dog\scenes\scene-over.js

import Phaser from 'phaser';

export default class extends Phaser.Scene {
  constructor() {
    super({ key: 'over' })
  }
  create(result) {
    const x = this.game.config.width / 2;
    const y = this.game.config.height / 2;

    const text = result === 'win' ? '恭喜获胜' : '哭哭,被打败了';
    const texture = result === 'win' ? 'cat-attack' : 'cat-beaten';

    // 主角
    this.cat = this.physics.add.sprite(x, y - 80, texture)
      .setScale(0.5);

    // 提示文字
    this.add.text(x, y + 50, text, {
      fill: '#000',
      fontSize: 30,
    }).setOrigin(0.5);

    this.add.text(x, y + 100, '按下摇杆按键重新开始', {
      fill: '#000',
      fontSize: 18,
    }).setOrigin(0.5);

    /** @type {JoyStick} */
    const joyStick = this.game.joyStick

    // 延迟一秒钟後再侦测摇杆按钮,防止一进到场景後误按按钮马上触发
    setTimeout(() => {
      joyStick.once('toggle', () => {
        this.scene.start('main');
      });
    }, 1000);
  }
}

最後让我们实测看看吧!

获胜结束画面

D31 - 获胜结束画面.gif

战败结束画面

D31 - 战败结束画面.gif

以上我们完成全部的游戏功能了,最後让我们复原为了方便开发调整的内容吧。

  • scene-preload.js 下一个场景改回 welcome
  • game-scene.vue 取消 config.physics.arcade.debug

src\components\window-app-cat-vs-dog\scenes\scene-preload.js

// ...

export default class extends Phaser.Scene {
  constructor() {
    super({ key: 'preload' })
  }

  // ...
  create() {
    // ...

    // 前往下一个场景
    this.scene.start('welcome');
  }
}

src\components\window-app-cat-vs-dog\game-scene.vue <script>

// ...

export default {
  name: 'GameScene',
  // ...
  methods: {
    /** 初始化游戏 */
    initGame() {
      /** @type {Phaser.Types.Core.GameConfig} */
      const config = {
        // ...

        physics: {
          default: 'arcade',
          arcade: {
            // debug: true,
          },
        },
      };

      // ...
    },

    // ...
  },
};

来正式玩一场吧!

D31 - 正式玩一场.gif

少少程序码就能完成功能的感觉是不是很棒呢

感恩 Phaser!赞叹 Phaser!◝( ゚ ∀。)◟

大家可以挑战看看更进阶的功能,例如:按着开关集气,可以发射出威力更强的武器、敌人不只会追击,还会闪躲子弹等等。

总结

  • 建立主角、敌人武器
  • 完成碰撞侦测
  • 完成结束场景
  • 完成「猫狗大战」视窗

以上程序码已同步至 GitLab,大家可以前往下载:

GitLab - D31


尾声

很感谢大家一参与这场为期 31 天的奇幻旅程。

大家的支持是我完赛的原动力,在此感谢大家。

有缘的话,让我们明年再见罗!✧*。٩(* ˊᗜˋ )ノ٩(ˊᗜˋ*)و✧*。


<<:  D30 - 「来互相伤害啊!」:猫狗集合!

>>:  Day 28 Flask-RESTX

CMoney菁英软件工程师战斗营前端课程_Week 11

Hi 本周已开始分领域课程 第一堂课不外乎就是介绍基本HTML语法 每周四都会有个小演习 这次题目是...

e是咱ㄟ宝贝

前面提到那个e,蕴含着我们与网页互动丰富的资讯。 例如: type:代表事件的类别。 target:...

Day 30-完赛心得

实作成果: (今过长时间的debug终於成功了 ) 不知不觉就30天了 从一开始的不适应,到现在习惯...

JavaScript Day22 - setTimeout、setInterval

setTimeout setTimeout:定时器,只执行一次,属於非同步,因此就算设定 0 秒执行...

蓝牙小知识

名称的由来 Bluetooth是斯堪地那维亚语言的Blåtand/Blåtann 借10世纪丹麦和...