本系列文已改編成書「Arduino 自造趣:結合 JavaScript x Vue x Phaser 輕鬆打造個人遊戲機」,本書改用 Vue3 與 TypeScript 全面重構且加上更詳細的說明,
在此感謝 iT 邦幫忙、博碩文化與編輯小 p 的協助,歡迎大家前往購書,鱈魚在此感謝大家 (。・∀・)。
若想 DIY 卻不知道零件去哪裡買的讀者,可以參考此連結 。( •̀ ω •́ )✧
上競技場就是要決鬥阿,不然要幹嘛。
來讓人物發射武器!血流成河吧!
首先來回顧一下 D27 武器規劃。
可以發現同一時間內會出現多個相同的武器,所以這時候要出動 Phaser 的 Group。
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
變數,儲存注入之武器constructor
之 params
參數加入 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,
});
// ...
}
}
試試看按下搖桿按鈕有沒有成功發射武器吧。
成功發射!但是主角停留在發射動畫,回到 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');
}
}
// ...
}
完成主角發射動畫!
接著來讓狗狗噴骨頭吧。
首先建立敵人武器 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);
}
}
可以看到狗狗開始很兇殘得丟骨頭了! ⎝(・ω´・⎝)
鱈魚:「再來就是人物與武器的激❤烈❤碰撞了!」
電子助教:「就不能用正常一點的方式描述碰撞偵測嘛 ...(´● ω ●`)」
加入人物扣血與勝敗部分,先將人物的血量顯示出來吧。
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}`);
}
}
可以看到畫面上多了貓命與狗血。
最後就是加入人物與武器的碰撞偵測了。
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();
});
}
// ...
}
可以看到主角與敵人被擊中時都會播放被擊中動畫,同時減少血量。
但是目前血量歸零後,不會有任何變化,所以要怎麼進到結束場景呢?
很簡單,由於我們已經在 sprite-cat.js
和 sprite-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);
}
}
最後讓我們實測看看吧!
以上我們完成全部的遊戲功能了,最後讓我們復原為了方便開發調整的內容吧。
scene-preload.js
下一個場景改回 welcomegame-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,
},
},
};
// ...
},
// ...
},
};
來正式玩一場吧!
少少程式碼就能完成功能的感覺是不是很棒呢
大家可以挑戰看看更進階的功能,例如:按著開關集氣,可以發射出威力更強的武器、敵人不只會追擊,還會閃躲子彈等等。
以上程式碼已同步至 GitLab,大家可以前往下載:
很感謝大家一參與這場為期 31 天的奇幻旅程。
大家的支持是我完賽的原動力,在此感謝大家。
有緣的話,讓我們明年再見囉!✧*。٩(* ˊᗜˋ )ノ٩(ˊᗜˋ*)و✧*。