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

有场景了,来让人物登场吧!(≧∀≦)

首先将场景载入游戏中。

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

/**
 * @typedef {import('@/script/modules/port-transceiver').default} PortTransceiver
 *
 * @typedef {import('@/types/type').PinInfo} PinInfo
 * @typedef {import('@/types/type').PinCapability} PinCapability
 */

import { mapState } from 'vuex';
import Phaser from 'phaser';

import ScenePreload from './scenes/scene-preload';
import SceneWelcome from './scenes/scene-welcome';
import SceneMain from './scenes/scene-main';
import SceneOver from './scenes/scene-over';

// ...

export default {
  name: 'GameScene',
  // ...
  methods: {
    /** 初始化游戏 */
    initGame() {
      /** @type {Phaser.Types.Core.GameConfig} */
      const config = {
        type: Phaser.WEBGL,
        width: 600,
        height: 800,
        parent: `game-scene-${this.id}`,
        scene: [ScenePreload, SceneWelcome, SceneMain, SceneOver],
        backgroundColor: '#FFF',
        disableContextMenu: true,

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

      this.game = new Phaser.Game(config);
    },

    // ...
  },
};

接着删除 initController() 中测试用的 onAny 监听程序,并将 joyStick 物件挂载至 game 物件中,让游戏中也能取用。

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

// ...

export default {
  name: 'GameScene',
  // ...
  methods: {
    /** 初始化游戏 */
    initGame() {
      /** @type {Phaser.Types.Core.GameConfig} */
      const config = {
        type: Phaser.WEBGL,
        width: 600,
        height: 800,
        parent: `game-scene-${this.id}`,
        scene: [ScenePreload, SceneWelcome, SceneMain, SceneOver],
        backgroundColor: '#FFF',
        disableContextMenu: true,

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

      this.game = new Phaser.Game(config);
      this.game.joyStick = this.joyStick;
    },

    /** 初始化摇杆 */
    initController() {
      this.joyStick = new JoyStick({
        // ...
      });
    },
  },
};

欢迎场景

让我们前往 scene-welcome.js 场景,首先让主角出现在场地上。

import Phaser from 'phaser';

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

    this.cat = this.physics.add
      .sprite(x, y - 80, 'cat-work')
      .setScale(0.5)
      .setCollideWorldBounds(true);
  }
  update() {
  }
}
  • xy 是场景尺寸,用来将主角定位至场地中央。
  • setScale() 可以用来控制人物尺寸,以免原本素材尺寸太小或太大。
  • setCollideWorldBounds() 设定人物会与世界边界发生碰撞,这样人物就不会冲出世界外。

D30 - 主角登场.gif

主角登场!

让我们加点动画吧!

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

// ...

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

    this.cat = this.physics.add
      .sprite(x, y - 80, 'cat-work')
      .setScale(0.5)
      .setCollideWorldBounds(true)
      .anims.play('cat-work');
  }
}

一行完成!

D30 - 主角一般动作.gif

是不是和之前小恐龙章节自干动画相比,相当的简单惬意呢。

感恩 Phaser!赞叹 Phaser!ᕕ( ゚ ∀。)ᕗ

接着加点提示文字吧。

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

// ...

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

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

Untitled

欢迎场景完成!只用了不到 20 行程序就完成的感觉真好。◝(≧∀≦)◟

所以说摇杆呢?

又差点忘了摇杆 (´,,•ω•,,)

把摇杆取到的类比摇杆数值,设为人物的速度,就可以让人物移动了!

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

/**
 * @typedef {import('@/script/electronic-components/joy-stick').default} JoyStick
 */

import Phaser from 'phaser';

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

    /** @type {JoyStick} */
    const joyStick = this.game.joyStick
    joyStick.on('data', ({ x, y }) => {
      this.cat.setVelocity(x, y);
    });
  }
}

D30 - 使用摇杆控制主角.gif

最後就是按下按钮後,进入下一个场景。

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

/**
 * @typedef {import('@/script/electronic-components/joy-stick').default} JoyStick
 */

import Phaser from 'phaser';

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

    /** @type {JoyStick} */
    const joyStick = this.game.joyStick
    joyStick.on('data', ({ x, y }) => {
      this.cat.setVelocity(x, y);
    }).once('toggle', () => {
      this.scene.start('main');
    });

    /** 监听 destroy 事件,清除所有摇杆监听器
     * 以免人物被销毁後,摇杆还持续呼叫 setVelocity,导致错误
     */
    this.cat.once('destroy', () => {
      joyStick.removeAllListeners();
    });
  }
}

D30 - 从欢迎场景进入主场景.gif

可以看到按下摇杆按钮後,主角和文字都不见了。

不是坏掉了,而是我们进入下一个场景了。

主场景

正式进入互相伤害场景!

为了方便测试,进行以下调整:

  • scene-preload.js 将下一个进入的场景从 welcome 改为 main。
  • game-scene.vue 游戏设定之 physics.arcade.debug 设为 true,如此会显示所有物体的碰撞边界与速度向量等等。

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

// ...

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

  preload() {
    // ...

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

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,
          },
        },
      };

      // ...
    },

    // ...
  },
};

接着在 scene-main.js 加入河流。

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

import Phaser from 'phaser';

export default class extends Phaser.Scene {
  constructor() {
    super({ key: 'main' })
  }
  preload() {
  }
  create() {
    // 加入中央河流
    this.platforms = this.physics.add.staticGroup();
    this.platforms.create(300, 400, 'river').setScale(0.17).refreshBody();
  }
}
  • staticGroup() 表示建立静态物体群组,用於存放静态物体。静态物体为不受重力影响、没有速度的物体,常用於地板、墙壁等等用途。
  • refreshBody() 用於让物体根据缩放尺寸调整碰撞箱尺寸,从以下比较图即可知道为甚麽。

未使用 refreshBody()

Untitled

加入 refreshBody()

Untitled

可以注意到使用 refreshBody() 後,河流的碰撞箱尺寸才是正确的尺寸。

加入主角

接下来准备加入人物吧,让我们复习一下 D27 中主角的设计。

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

  • 主角
    • 透过摇杆控制人物移动,按下按钮发射武器
    • 按下按钮发射武器,并播放发射动画
    • 血量显示在左上角,预设 5 点
    • 被敌人武器击中时,播放被击中动画并减少生命值
    • 生命值归零时,触发死亡事件

由於主场景中的主角有多个程序逻辑,直接将程序写在场景中会让程序难以维护,所以将主角独立一个 class 吧!

新增 src\components\window-app-cat-vs-dog\objects 目录,用来存放各种人物 class。

新增主角档案并加入基本内容。

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

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

import Phaser from 'phaser';

export default class extends Phaser.Physics.Arcade.Sprite {
  /** 血量 */
  health = 5;

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

    super(scene, x, y, 'cat-work');

    // 将人物加入至场景并加入物理系统
    scene.add.existing(this);
    scene.physics.add.existing(this);

    // 设定人物缩放、碰撞箱尺寸、碰撞反弹、世界边界碰撞
    this.setScale(0.3)
      .setSize(220, 210)
      .setBounce(0.2)
      .setCollideWorldBounds(true);

    // 播放动画
    this.play('cat-work');

    this.scene = scene;
  }

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

回到 scene-main.js 引入主角并建立物件。

import Phaser from 'phaser';

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

export default class extends Phaser.Scene {
  constructor() {
    super({ key: 'main' })
  }
  preload() {
  }
  create() {
    // 建立主角
    this.cat = new SpriteCat(this);

    // 加入中央河流
    this.platforms = this.physics.add.staticGroup();
    this.platforms.create(300, 400, 'river').setScale(0.17).refreshBody();

  }
}

试试看主角有没有成功登场。

D30 - 建立主角物件.gif

成功!猫猫动起来了!

接着加入摇杆控制人物速度的部分,透过转为单位向量的方式限制人物速度。

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

/**
 * @typedef {import('@/script/electronic-components/joy-stick').default} JoyStick
 * 
 * @typedef {Object} CatParams
 * @property {number} [x]
 * @property {number} [y]
 */

import Phaser from 'phaser';

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

export default class extends Phaser.Physics.Arcade.Sprite {
  // ...
  constructor(scene, params = {}) {
    // ...

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

    joyStick.on('data', ({ x, y }) => {
      // 将 x、y 数值组合为向量并转为单位向量。
      const velocityVector = new Phaser.Math.Vector2(x, y);
      velocityVector.normalize();

      // 将单位向量 x、y 分量分别乘上最大速度
      const { x: vx, y: vy } = velocityVector;
      this.setVelocity(vx * velocityMax, vy * velocityMax);
    });

    this.once('destroy', () => {
      joyStick.removeAllListeners();
    });
  }

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

D30 - 主场景主角移动.gif

主角可以移动了,但是发现一个问题,主角竟然可以穿过河流。

这是因为没有加上碰撞,回到 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';

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

    // 加入河流与人物碰撞
    this.physics.add.collider([this.cat, this.platforms]);
  }
}

一行完成!

D30 - 主角与河流碰撞.gif

可以看到主角现在没办法轻功水上飘了。

最後在主角 class 中加入生命值相关的 method

// ...

export default class extends Phaser.Physics.Arcade.Sprite {
  // ...

  /** 取得生命值 */
  getHealth() {
    return this.health;
  }
  /** 扣血 */
  subHealth(val = 1) {
    this.health = Phaser.Math.MinSub(this.health, val, 0);
    this.play('cat-beaten', true);

    if (this.health === 0) {
      this.emit('death');
    }
  }
}

Phaser.Math.MinSub() 可以指定减法结果最小值,可以省去自己判断是否减过头的工作。

加入敌人

回顾一下设计。

  • 上下随机移动,左右则追着主角移动
  • 随机发射武器并播放发射动画
  • 血量显示在左上角,预设 10 点
  • 被主角武器击中时,播放被击中动画并减少生命值
  • 生命值归零时,触发死亡事件

基本概念与主角完全相同,差别在输入参数多一个 target,用来表示要追击的目标。

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

/**
 * @typedef {Object} DogParams
 * @property {number} [x]
 * @property {number} [y]
 * @property {Phaser.Physics.Arcade.Sprite} target
 */

import Phaser from 'phaser';

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

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

    super(scene, x, y, 'dog-work');
    scene.add.existing(this);
    scene.physics.add.existing(this);

    this.setScale(0.2)
      .setSize(340, 420)
      .setCollideWorldBounds(true);
    this.play('dog-work');

    this.scene = scene;
    this.target = target;
  }

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

  /** 取得生命值 */
  getHealth() {
    return this.health;
  }
  /** 扣血 */
  subHealth(val = 1) {
    this.health = Phaser.Math.MinSub(this.health, val, 0);
    this.play('dog-beaten', true);

    if (this.health === 0) {
      this.emit('death');
    }
  }
}

加入计时器让敌人动起来。

// ...

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

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

    this.initAutomata();
  }

  // ...

  initAutomata() {
    // 追猫
    this.scene.time.addEvent({
      delay: 500,
      callbackScope: this,
      repeat: -1,
      callback: async () => {
        const vx = (this.target.x - this.x) * 1.5;
        const vy = Phaser.Math.Between(-400, 400);

        this.setVelocity(vx, vy);
      },
    });
  }
}
  • X 方向速度为与目标之差值,这样就会让敌人持续往目标的方向前进。
  • Y 方向则随机运动。

大家也可以自行设计更强大的敌人 AI 喔

现在回到 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';

export default class extends Phaser.Scene {
  constructor() {
    super({ key: 'main' })
  }
  create() {
    this.cat = new SpriteCat(this);

    this.dog = new SpriteDog(this, {
      target: this.cat,
    });

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

    // 加入河流与人物碰撞
    this.physics.add.collider([this.cat, this.dog, this.platforms]);
  }
}

D30 - 敌人登场.gif

可以看到敌人成功登场,而且会持续追着主角移动了。

Phaser 处理完多种繁琐细节,我们只要专注於游戏逻辑即可,感觉是不是很棒啊

感恩 Phaser!赞叹 Phaser!ᕕ( ゚ ∀。)ᕗ

电子助教:「这是甚麽奇怪的宗教吗?...(́⊙◞౪◟⊙‵)」


就算砍了很多内容结果还是不小心写超过 30 篇了 Σ(ˊДˋ;)
下次会好好调整内容,还请大家继续看下去

总结

  • 建立主角物件
  • 建立敌人物件

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

GitLab - D30


<<:  【第二九天 - Flutter 开发套件之旅(下)】

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

[2021铁人赛 Day25] Web Exploitation Web渗透题目 01

引言 今天我们来解 Web 渗透 ( 渗透测试 ) 的题目, 在这之前,你需要先初步了解 HTTP...

Day 23 - 绿专案管理(Green Project Management)

图片来源 继续延续前几篇的话题, 好巧不巧本月(2021年10月)刚出刊的专案经理杂志的封面故事,...

[Day12] 让 Linux 的 systemd 帮我们管理 API 程序

昨天我们成功的把 API 程序布署到 GCP 的 VM 上了。不过,我们有一个问题:只要跑了 .NE...

JS Library 学习笔记:首先当然来试试 jQuery (一)

要撰写前端功能,直接使用JavaScript是绝对可行的,但要更有效率、具有良好开发体验的话,使用L...

DAY 12 SASS 间的相似之处

介绍完了前几天的 sass 各种用法,大家有没有觉得有些方法好像很类似? 像是 mixin 跟 ex...