有场景了,来让人物登场吧!(≧∀≦)
首先将场景载入游戏中。
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,大家可以前往下载:
<<: 【第二九天 - Flutter 开发套件之旅(下)】
>>: D31 - 「来互相伤害啊!」:无聊我要见到血流成河
引言 今天我们来解 Web 渗透 ( 渗透测试 ) 的题目, 在这之前,你需要先初步了解 HTTP...
图片来源 继续延续前几篇的话题, 好巧不巧本月(2021年10月)刚出刊的专案经理杂志的封面故事,...
昨天我们成功的把 API 程序布署到 GCP 的 VM 上了。不过,我们有一个问题:只要跑了 .NE...
要撰写前端功能,直接使用JavaScript是绝对可行的,但要更有效率、具有良好开发体验的话,使用L...
介绍完了前几天的 sass 各种用法,大家有没有觉得有些方法好像很类似? 像是 mixin 跟 ex...