本系列文已改編成書「Arduino 自造趣:結合 JavaScript x Vue x Phaser 輕鬆打造個人遊戲機」,本書改用 Vue3 與 TypeScript 全面重構且加上更詳細的說明,
在此感謝 iT 邦幫忙、博碩文化與編輯小 p 的協助,歡迎大家前往購書,鱈魚在此感謝大家 (。・∀・)。
若想 DIY 卻不知道零件去哪裡買的讀者,可以參考此連結 。( •̀ ω •́ )✧
有場景了,來讓人物登場吧!(≧∀≦)
首先將場景載入遊戲中。
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() {
}
}
x
、y
是場景尺寸,用來將主角定位至場地中央。setScale()
可以用來控制人物尺寸,以免原本素材尺寸太小或太大。setCollideWorldBounds()
設定人物會與世界邊界發生碰撞,這樣人物就不會衝出世界外。主角登場!
讓我們加點動畫吧!
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');
}
}
一行完成!
是不是和之前小恐龍章節自幹動畫相比,相當的簡單愜意呢。
接著加點提示文字吧。
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);
}
}
歡迎場景完成!只用了不到 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);
});
}
}
最後就是按下按鈕後,進入下一個場景。
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();
});
}
}
可以看到按下搖桿按鈕後,主角和文字都不見了。
不是壞掉了,而是我們進入下一個場景了。
正式進入互相傷害場景!
為了方便測試,進行以下調整:
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()
加入 refreshBody()
可以注意到使用 refreshBody()
後,河流的碰撞箱尺寸才是正確的尺寸。
接下來準備加入人物吧,讓我們複習一下 D27 中主角的設計。
由於主場景中的主角有多個程式邏輯,直接將程式寫在場景中會讓程式難以維護,所以將主角獨立一個 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();
}
}
試試看主角有沒有成功登場。
成功!貓貓動起來了!
接著加入搖桿控制人物速度的部分,透過轉為單位向量的方式限制人物速度。
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);
}
}
主角可以移動了,但是發現一個問題,主角竟然可以穿過河流。
這是因為沒有加上碰撞,回到 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]);
}
}
一行完成!
可以看到主角現在沒辦法輕功水上飄了。
最後在主角 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()
可以指定減法結果最小值,可以省去自己判斷是否減過頭的工作。
回顧一下設計。
基本概念與主角完全相同,差別在輸入參數多一個 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);
},
});
}
}
大家也可以自行設計更強大的敵人 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]);
}
}
可以看到敵人成功登場,而且會持續追著主角移動了。
Phaser 處理完多種繁瑣細節,我們只要專注於遊戲邏輯即可,感覺是不是很棒啊
電子助教:「這是甚麼奇怪的宗教嗎?...(́⊙◞౪◟⊙‵)」
就算砍了很多內容結果還是不小心寫超過 30 篇了 Σ(ˊДˋ;)
下次會好好調整內容,還請大家繼續看下去
以上程式碼已同步至 GitLab,大家可以前往下載: