先建立游戏组件,以便加入後续内容。
src\components\window-app-cat-vs-dog\game-scene.vue <template lang="pug">
.game-scene.w-600px.h-800px(:id='`game-scene-${id}`')
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';
export default {
name: 'GameScene',
data() {
return {
/** @type {Phaser.Game} */
game: null,
};
},
mounted() {
this.initGame();
},
methods: {
/** 初始化游戏 */
initGame() {
/** @type {Phaser.Types.Core.GameConfig} */
const config = {
type: Phaser.WEBGL,
width: 600,
height: 800,
parent: `game-scene-${this.id}`,
scene: [],
backgroundColor: '#FFF',
disableContextMenu: true,
physics: {
default: 'arcade',
arcade: {
// debug: true,
},
},
};
this.game = new Phaser.Game(config);
},
},
};
建立视窗,打开 DevTools,应该会在 Console 中看到以下讯息。
这就表示我们成功载入 Phaser 了!
接下来让我们加入摇杆并完成设定栏位吧。
先来认识新朋友,相信任何有过游戏机的玩家应该都有用过。
没用过摇杆,也有看过摇杆走路。ᕕ( ゚ ∀。)ᕗ
两个可变电阻刚好相交 90 度角,如此便可以表示一个平面上的运动。
接下来让我们组一个电路,实际看看摇杆模组的讯号。
组电路之前一样先来检查摇杆功能是否正常。
基本上概念等测试「可变电阻」与「按钮」。
透过先前建立的「数位 I/O 视窗」与「类比输入视窗」来看看讯号吧!
首先完成电路接线。
接着建立视窗实测看看。
鳕鱼:「让我们回到 D12 与 D15 一样,开始解析数位讯号与类比讯号吧!」
电子助教:「同一个段子用两次会没人要看喔 ...('◉◞⊖◟◉` )」
鳕鱼:「窝错惹,请大家不要离开 ( ´•̥̥̥ ω •̥̥̥` )」
与 D25 时建立按钮物件一样,来做一个摇杆物件吧!
建立 joy-stick.js
用於摇杆讯号转换、抽象化,方便应用。
button.js
,并转发所有按钮事件。src\script\electronic-components\joy-stick.js
/**
* @typedef {import('EventEmitter2').Listener} Listener
*
* @typedef {import('@/script/modules/port-transceiver').default} PortTransceiver
*
* @typedef {import('@/types/type').PinInfo} PinInfo
* @typedef {import('@/script/firmata/firmata').DigitalResponseMessage} DigitalResponseMessage
* @typedef {import('@/script/firmata/firmata').AnalogResponseMessage} AnalogResponseMessage
* @typedef {import('@/script/electronic-components/Button').ConstructorParams} ButtonParams
*
* @typedef {Object} ConstructorParams
* @property {PortTransceiver} transceiver Port 收发器
* @property {Object} analogPinMap 类比脚位映射表
* @property {AxisOptions} xAxis X 轴设定
* @property {AxisOptions} yAxis Y 轴设定
* @property {Object} btn 按钮设定
* @property {PinInfo} btn.pin 指定脚位
* @property {number} btn.mode 按钮脚位模式
*
* @typedef {Object} AxisOptions 轴设定
* @property {PinInfo} pin 指定脚位
* @property {number} [origin] 原点。摇杆无动作时,类比数值基准点。
* @property {number} [threshold] 阀值。origin 正负 threshold 之内的数值,视为没有动作。
* @property {boolean} [isReverse] 轴反转
*
*/
import EventEmitter2 from 'eventemitter2';
import { findLast, throttle } from 'lodash-es';
import { PinMode } from '@/script/utils/firmata.utils';
import Button from './button';
const axisOptionsDefault = {
origin: 510,
threshold: 20,
isReverse: false,
};
/**
* 基本摇杆
*/
export default class extends EventEmitter2.EventEmitter2 {
/** 目前轴类比数值 */
axesValue = {
x: 0,
y: 0,
};
/** @type {PortTransceiver} */
portTransceiver = null;
analogPinMap = {};
/** X 轴设定
* @type {AxisOptions}
*/
xAxis = null;
/** Y 轴设定
* @type {AxisOptions}
*/
yAxis = null;
/** 按钮物件
* @type {Button}
*/
btn = null;
/** 数值回报监听器
* @type {Listener}
*/
listener = null;
throttle = {
setAxesValue: null,
};
/**
* @param {ConstructorParams} params
*/
constructor(params) {
super();
const {
transceiver = null,
analogPinMap = null,
xAxis, yAxis, btn
} = params;
if (!transceiver) throw new Error(`transceiver 必填`);
if (!analogPinMap) throw new Error(`analogPinMap 必填`);
if (!xAxis?.pin) throw new Error(`xAxis.pin 必填`);
if (!yAxis?.pin) throw new Error(`yAxis.pin 必填`);
if (!btn?.pin) throw new Error(`btn.pin 必填`);
// 储存变数
this.portTransceiver = transceiver;
this.analogPinMap = analogPinMap;
this.xAxis = {
...axisOptionsDefault, ...xAxis
}
this.yAxis = {
...axisOptionsDefault, ...yAxis
}
/** 初始化按钮物件 */
this.btn = new Button({
...btn,
transceiver,
});
/** 将所有 btn 事件转送出去 */
this.btn.onAny((event, value) => this.emit(event, value));
/** 建立 throttle 功能 */
this.throttle = {
setAxesValue: throttle((...params) => {
this.setAxesValue(...params);
}, 100),
}
this.init();
}
/** 初始化
* 进行脚位设定、启动监听器
*/
async init() {
const xPinNum = this.xAxis.pin.number;
const yPinNum = this.yAxis.pin.number;
this.portTransceiver.addCmd('setMode', {
pin: xPinNum,
mode: PinMode.ANALOG_INPUT,
});
this.portTransceiver.addCmd('setMode', {
pin: yPinNum,
mode: PinMode.ANALOG_INPUT,
});
this.listener = this.portTransceiver.on('data:analogMessage', (data) => {
this.handleData(data);
}, { objectify: true });
}
/** 销毁所有物件、监听器 */
destroy() {
this.btn.destroy();
this.listener.off();
this.removeAllListeners();
}
/** 处理类比讯号数值
* @param {AnalogResponseMessage[]} data
*/
handleData(data) {
const { xAxis, yAxis, analogPinMap } = this;
let x = 0;
let y = 0;
/** 取得 X 轴类比资料 */
const xVal = findLast(data, ({ analogPin }) => {
const mapNum = analogPinMap[xAxis.pin.number];
return mapNum === analogPin
});
if (xVal) {
x = this.calcAxisValue(xVal.value, xAxis)
}
/** 取得 Y 轴类比资料 */
const yVal = findLast(data, ({ analogPin }) => {
const mapNum = analogPinMap[yAxis.pin.number];
return mapNum === analogPin
});
if (yVal) {
y = this.calcAxisValue(yVal.value, yAxis)
}
this.throttle.setAxesValue({
x, y,
});
}
/** 储存轴向类比数值 */
setAxesValue({ x, y }) {
this.axesValue.x = x;
this.axesValue.y = y;
this.emit('data', this.axesValue);
}
/** 取得轴向类比数值 */
getAxesValue() {
return this.axesValue;
}
/** 取得按钮数值 */
getBtnValue() {
return this.btn.getValue();
}
/** 将类比数值转换为摇杆轴资料
* @param {number} value
* @param {AxisOptions} options
*/
calcAxisValue(value, options) {
const { origin, threshold, isReverse } = options;
/**
* 需要设定 threshold,因为实际上摇杆回到中点时,
* 类比数值并非完全静止,可能会在正负 1 或 2 些微浮动,
* 如果判断摇杆静止是用单一数值判断,容易误判,
* 所以透过 threshold,以范围来判断,就可以解决误判问题。
*/
const delta = origin - value;
if (Math.abs(delta) < threshold) {
return 0;
}
return isReverse ? delta * -1 : delta;
}
}
了解摇杆後,现在我们知道总共需要设定 3 个脚位,让我们回到 window-app-cat-vs-dog.vue
完成设定栏位选项。
data()
xPin
、yPin
、btnPin
computed
supportPullupPins()
列举支援上拉输入脚位清单supportAnalogInputPins()
列举支援类比输入脚位清单isSettingOk()
程序。watch
xPin
、yPin
、btnPin
变化,并呼叫 handlePinSelect()
src\components\window-app-cat-vs-dog\window-app-cat-vs-dog.vue <script>
// ...
import { PinMode } from '@/script/utils/firmata.utils';
const { INPUT_PULLUP, ANALOG_INPUT } = PinMode;
export default {
name: 'WindowAppCatVsDog',
// ...
data() {
return {
xPin: null,
yPin: null,
btnPin: null,
};
},
computed: {
...mapState({
boardPins: (state) => state.board.info.pins,
}),
/** 支援上拉输入功能脚位 */
supportPullupPins() {
/** @type {PinInfo[]} */
const boardPins = this.boardPins;
return boardPins.filter((pin) =>
pin.capabilities.some((capability) => INPUT_PULLUP === capability.mode)
);
},
/** 支援类比输入功能脚位 */
supportAnalogInputPins() {
/** @type {PinInfo[]} */
const boardPins = this.boardPins;
return boardPins.filter((pin) =>
pin.capabilities.some((capability) => ANALOG_INPUT === capability.mode)
);
},
/** 设定栏位是否完成 */
isSettingOk() {
return this.xPin && this.yPin && this.btnPin;
},
},
watch: {
xPin(newVal, oldVal) {
this.handlePinSelect(newVal, oldVal);
},
yPin(newVal, oldVal) {
this.handlePinSelect(newVal, oldVal);
},
btnPin(newVal, oldVal) {
this.handlePinSelect(newVal, oldVal);
},
},
// ...
};
src\components\window-app-cat-vs-dog\window-app-cat-vs-dog.vue <template lang="pug">
base-window.window-app-cat-vs-dog(
:pos='pos',
header-icon='r_videogame_asset',
body-class='c-col',
title='猫狗大战'
)
.h-full.overflow-hidden
game-scene
transition(name='fade-up')
.setting-form(v-if='!isSettingOk')
.form-section
.form-item.mb-20px
q-icon.mr-10px(name='r_gamepad', size='20px')
.text-18px.font-700
| 设定控制器
.form-item
.text-16px.w-200px
| X 轴讯号
base-select-pin.w-full(
v-model='xPin',
:pins='supportAnalogInputPins',
color='light-green-4',
placeholder='点击选择',
@err='handleErr'
)
.form-item
.text-16px.w-200px
| Y 轴讯号
base-select-pin.w-full(
v-model='yPin',
:pins='supportAnalogInputPins',
color='light-green-4',
placeholder='点击选择',
@err='handleErr'
)
.form-item
.text-16px.w-200px
| 按钮讯号
base-select-pin.w-full(
v-model='btnPin',
:pins='supportPullupPins',
color='light-green-4',
placeholder='点击选择',
@err='handleErr'
)
完成後应该会长这样。
将着将脚位透过 props
传入 game-scene.vue
组件吧。
src\components\window-app-cat-vs-dog\game-scene.vue <script>
// ...
export default {
name: 'GameScene',
props: {
xPin: {
type: Object,
default() {
return null;
},
},
yPin: {
type: Object,
default() {
return null;
},
},
btnPin: {
type: Object,
default() {
return null;
},
},
},
// ...
};
window-app-cat-vs-dog.vue
模板中的 game-scene.vue
也要记得加入参数。
src\components\window-app-cat-vs-dog\window-app-cat-vs-dog.vue <template lang="pug">
base-window.window-app-cat-vs-dog(
:pos='pos',
header-icon='r_videogame_asset',
body-class='c-col',
title='猫狗大战'
)
.h-full.overflow-hidden
game-scene(:x-pin='xPin', :y-pin='yPin', :btn-pin='btnPin')
// ...
接着侦测是否所有的脚位都设定完成,完成後进行摇杆初始化。
data()
新增 joyStick
,储存摇杆物件。portTransceiver
与 analogPinMap
。mounted()
内的 this.initGame()
,改由其他程序负责呼叫初始化。methods()
新增 initController()
,用於初始化摇杆。created()
使用 $watch
进行所有脚位侦测与初始化。beforeDestroy()
销毁所有物件。src\components\window-app-cat-vs-dog\game-scene.vue <script>
// ...
import JoyStick from '@/script/electronic-components/joy-stick';
import { PinMode } from '@/script/utils/firmata.utils';
const { INPUT_PULLUP } = PinMode;
export default {
name: 'GameScene',
// ...
data() {
return {
/** @type {Phaser.Game} */
game: null,
/** @type {JoyStick} */
joyStick: null,
};
},
computed: {
...mapState({
/** @type {PortTransceiver} */
portTransceiver: (state) => state.core.transceiver,
analogPinMap: (state) => state.board.info.analogPinMap,
}),
},
// ...
created() {
// 同时侦测所有 Pin
this.$watch(
() => {
return [this.xPin, this.yPin, this.btnPin];
},
([xPin, yPin, btnPin]) => {
// 所有脚位都设定完成才初始化
if (!xPin || !yPin || !btnPin) return;
this.initController();
this.initGame();
}
);
},
mounted() {
// 清空
},
beforeDestroy() {
this.game?.destroy?.();
this.joyStick?.destroy?.();
},
methods: {
// ...
/** 初始化摇杆 */
initController() {
this.joyStick = new JoyStick({
transceiver: this.portTransceiver,
analogPinMap: this.analogPinMap,
xAxis: {
pin: this.xPin,
},
yAxis: {
pin: this.yPin,
},
btn: {
pin: this.btnPin,
mode: INPUT_PULLUP,
},
});
},
},
};
忽然发现建立视窗时有个错误。
这是因为忘记使用 inject
将 provide
进来的 id
取出来,就直接使用 id
造成的错误。
修正一下这个问题。
src\components\window-app-cat-vs-dog\game-scene.vue <script>
// ...
export default {
name: 'GameScene',
// ...
inject: ['id'],
// ...
};
完成控制器设定!
加入监听事件看看摇杆物件有没有正常作用吧。
在 initController()
中加入 onAny()
。
src\components\window-app-cat-vs-dog\game-scene.vue <script>
// ...
import JoyStick from '@/script/electronic-components/joy-stick';
import { PinMode } from '@/script/utils/firmata.utils';
const { INPUT_PULLUP } = PinMode;
export default {
name: 'GameScene',
// ...
methods: {
// ...
/** 初始化摇杆 */
initController() {
this.joyStick = new JoyStick({
// ...
});
this.joyStick.onAny((event, value) => {
if (event === 'data') {
const { x, y } = value;
console.log(`[ joyStick on ${event} ] value : `, x, y);
return;
}
console.log(`[ joyStick on ${event} ] value : `, value);
});
},
},
};
摇杆功能正常!一起玩摇杆吧!✧*。٩(ˊᗜˋ*)و✧*。
以上程序码已同步至 GitLab,大家可以前往下载:
<<: 【设计+切版30天实作】|Day28 - CTA区块 - 超快速切出简约CTA,让使用者注册起来!!
>>: [ Day 27 ] 实作一个 React.js 网站 3/5
Azure face service: Face recognition- 让你的机器人认得你 人脸...
前言 今天来看看, node 怎麽进行一个 http request 正文 打开 http.js 这...
运用到的观念 border搭配伪元素制作出三角形区块 绝对定位&相对定位 用:hover ...
接续着上篇的内容,这篇要介绍情境二、三~ 情境二:引用到其他的java档 Step1.先创建资料夹及...
在这个演算法当道的时代 每一家网路公司在想办法尽量的搜集使用者的资讯 不论是苹果限制脸书获取使用者的...