小恐龙现在就像吃了无敌星星一样,完全无视仙人掌,所以我们来让小恐龙死翘翘吧!
我们来加上「小恐龙」与「仙人掌」的碰撞侦测。
这里我们使用最简单「矩形碰撞」,简单来说就是将所有物体视为一个矩形,判断两个矩形是否有重叠。
对碰撞侦测细节有兴趣的朋友,可以参考以下连结:
矩形的碰撞侦测
“等一下,我碰!”——常见的2D碰撞检测
在 game-scene.vue
新增碰撞侦测用的 method
src\components\window-app-google-dino\game-scene.vue <script>
// ...
export default {
name: 'GameScene',
// ...
methods: {
// ...
/** 检查两个 DOM 是否重叠
* @param {HTMLElement} dom01
* @param {HTMLElement} dom02
* @param {number} tolerance 容差值
*/
isOverlap(dom01, dom02, tolerance = 20) {
const dom01Rect = dom01.getBoundingClientRect();
const dom02Rect = dom02.getBoundingClientRect();
const dom01Left = dom01Rect.left + tolerance;
const dom01Top = dom01Rect.top + tolerance;
const dom01Right = dom01Left + dom01Rect.width - tolerance;
const dom01Bottom = dom01Top + dom01Rect.height - tolerance;
const dom02Left = dom02Rect.left + tolerance;
const dom02Top = dom02Rect.top + tolerance;
const dom02Right = dom02Left + dom02Rect.width - tolerance;
const dom02Bottom = dom02Top + dom02Rect.height - tolerance;
// 检查重叠
if (
dom02Bottom > dom01Top &&
dom02Right > dom01Left &&
dom02Left < dom01Right &&
dom02Top < dom01Bottom
) {
return true;
}
return false;
},
},
};
接着就是将小恐龙和仙人掌的 DOM 取出来用了。
仙人掌 methods
新增 getDoms()
src\components\window-app-google-dino\cactuses.vue <script>
// ...
export default {
name: 'Cactuses',
// ...
methods: {
// ...
/** 取出所有仙人掌 DOM */
getDoms() {
return this.$refs?.cactus ?? [];
},
},
};
模板中记得加入 ref
。
src\components\window-app-google-dino\cactuses.vue <template lang="pug">
.cactuses
img.cactus(
ref='cactus',
// ...
)
接着是小恐龙的部分。
src\components\window-app-google-dino\dino.vue <script>
// ...
export default {
name: 'Dino',
// ...
methods: {
// ...
/** 取出 DOM */
getDom() {
return this.$refs?.dino;
},
},
};
src\components\window-app-google-dino\dino.vue <template lang="pug">
.dino(
// ...
)
img(ref='dino', :src='imgSrc')
最後在 game-scene.vue
的 tick()
不断检查碰撞。
src\components\window-app-google-dino\game-scene.vue <script>
// ...
export default {
name: 'GameScene',
// ...
methods: {
// ...
tick() {
this.timeCounter++;
// score 每 0.1 秒增加一次
if (this.timeCounter % 10 === 0) {
this.score++;
}
// 碰撞侦测
/** @type {HTMLElement[]} */
const cactuses = this.$refs.cactuses.getDoms();
/** @type {HTMLElement} */
const dino = this.$refs.dino.getDom();
// 检查是否有任一个仙人掌与小恐龙重叠
const isBump = cactuses.some((cactus) => this.isOverlap(dino, cactus));
if (isBump) {
this.over();
}
},
},
};
小恐龙成功撞上仙人掌!
现在游戏逻辑全部完成!只是出现一个神奇的问题。
小恐龙学会舞空术啦!╭(°A ,°`)╮
游戏重启後小恐龙没有回归原位,这是因为没有重置 GSAP 动画,导致动画还停留在上次位置。
所以在 dino.vue
的 start()
中加入重置 GSAP 的部分。
src\components\window-app-google-dino\dino.vue <script>
// ...
export default {
name: 'Dino',
// ...
methods: {
/** 开始 */
start() {
// 将动画进度设在起点
this.gsapAni.jump?.progress(0);
this.status.jumping = false;
// ...
},
// ...
},
};
成功废除舞空术 (ง •̀_•́)ง
成功修正小恐龙浮空问题。
我们成功重现恐龙游戏了!那就下一个章节见罗!
对吼,差点忘了要用实体按钮控制 (゚∀。)。
鳕鱼:「让我们回到 game-scene.vue
准备像 D12 一样,开始解析数位讯号吧!」
电子助教:「蛤,一样的事情要这样一直重复做喔 ...('◉◞⊖◟◉` )」
鳕鱼:「也是捏,这样违反 DRY 原则」
电子助教:「乾燥原则?」
鳕鱼:「是 Don't repeat yourself 原则」
详细说明可以参考连结:DRY 原则
所以我们将数位讯号处理的过程封装一下吧!
建立 button.js
用於将数位输入讯号转换成按钮行为,方便应用。
给定脚位、模式、收发器,自动处理 Port 转换、数位讯号监听等等逻辑。
主动通知按钮「按下」、「放开」等等事件。
将上拉输入反转,变为较为直觉的讯号呈现。
上拉输入按下按钮为 0,放开为 1。
一般符合直觉的讯号应该是按下为 1,放开为 0。
程序内容基本上同 D12 数位控制组件过程相同,差在多了 prcoessEvent()
处理事件。
src\script\electronic-components\button.js
/**
* @typedef {import('@/script/modules/port-transceiver').default} PortTransceiver
* @typedef {import('eventemitter2').Listener} Listener
*
* @typedef {import('@/types/type').PinInfo} PinInfo
* @typedef {import('@/script/firmata/firmata').DigitalResponseMessage} DigitalResponseMessage
*
* @typedef {Object} ConstructorParams
* @property {PinInfo} pin
* @property {number} mode 脚位模式
* @property {PortTransceiver} transceiver
* @property {Number} [debounceMillisecond] 去弹跳时长
*/
import EventEmitter2 from 'eventemitter2';
import { findLast, debounce } from 'lodash-es';
import { delay, getBitWithNumber } from '@/script/utils/utils';
import { PinMode } from '@/script/utils/firmata.utils';
const { DIGITAL_INPUT, INPUT_PULLUP } = PinMode;
/**
* 基本按钮
* 支援数位输入、上拉输入
*/
export default class extends EventEmitter2.EventEmitter2 {
/** 指定脚位
* @type {PinInfo}
*/
pin = null;
/** 脚位 Port 号 */
portNum = 0;
/** 脚位模式 */
mode = 0;
/** COM Port 收发器
* @type {PortTransceiver}
*/
portTransceiver = null;
/** 讯号回报监听器
* @type {Listener}
*/
listener = null;
/** 目前数位讯号 */
value = false;
/**
* @param {ConstructorParams} params
*/
constructor(params) {
super();
const {
pin = null,
transceiver = null,
mode = DIGITAL_INPUT,
debounceMillisecond = 10,
} = params;
if (!pin) throw new Error(`pin 必填`);
if (!transceiver) throw new Error(`transceiver 必填`);
if (![DIGITAL_INPUT, INPUT_PULLUP].includes(mode)) {
throw new Error(`不支援指定的脚位模式 : ${mode}`);
}
this.pin = pin;
this.portNum = (pin.number / 8) | 0;
this.mode = mode;
this.portTransceiver = transceiver;
this.options = {
debounceMillisecond
}
this.debounce = {
prcoessEvent: debounce((...params) => {
this.prcoessEvent(...params)
}, debounceMillisecond),
}
this.init();
}
async init() {
this.portTransceiver.addCmd('setMode', {
pin: this.pin.number,
mode: this.mode,
});
// 延迟一下再监听数值,忽略 setMode 初始化的数值变化
await delay(500);
this.listener = this.portTransceiver.on(
'data:digitalMessage',
(data) => {
this.handleData(data);
},
{ objectify: true }
);
}
destroy() {
// 销毁所有监听器,以免 Memory Leak
this.listener?.off?.();
this.removeAllListeners();
}
/** 处理数值
* @param {DigitalResponseMessage[]} data
*/
handleData(data) {
const target = findLast(data, ({ port }) => this.portNum === port);
if (!target) return;
const { value } = target;
/** @type {PinInfo} */
const pin = this.pin;
const bitIndex = pin.number % 8;
const pinValue = getBitWithNumber(value, bitIndex);
this.debounce.prcoessEvent(pinValue);
}
/** 依照数位讯号判断按钮事件
* - rising:上缘,表示放开按钮
* - falling:下缘,表示按下按钮
* - toggle:讯号切换,放开、按下都触发
*
* 参考资料:
* [讯号边缘](https://zh.wikipedia.org/wiki/%E4%BF%A1%E5%8F%B7%E8%BE%B9%E7%BC%98)
*
* @param {boolean} value
*/
prcoessEvent(value) {
let correctionValue = value;
// 若为上拉输入,则自动反向
if (this.mode === INPUT_PULLUP) {
correctionValue = !correctionValue;
}
if (this.value === correctionValue) return;
if (correctionValue) {
this.emit('rising');
}
if (!correctionValue) {
this.emit('falling');
}
this.emit('toggle', correctionValue);
this.value = correctionValue;
}
getValue() {
if (this.mode === INPUT_PULLUP) {
return !this.value;
}
return this.value;
}
}
大家还可以思考看看怎麽加入按住指定时间、双击等等事件喔!
接着把测试用的 click()
事件都删除。
不想删除,想留着玩也是可以 ヾ(◍'౪`◍)ノ゙
src\components\window-app-google-dino\dino.vue <template lang="pug">
.dino(:style='style')
// ...
src\components\window-app-google-dino\game-scene.vue <template lang="pug">
.game-scene
// ...
接着加入以下功能:
computed
与 watch
配合,侦测 props
两个输入脚位是否都选择完成。props
选择脚位初始化 button
物件button
物件控制角色。beforeDestroy()
中销毁所有物件与监听器。// ...
import Button from '@/script/electronic-components/button';
// ...
export default {
name: 'GameScene',
// ...
data() {
return {
gameId: '',
gameStatus: GameStatus.STANDBY,
timer: null,
timeCounter: 0,
score: 0,
/** @type {Button} */
jumpButton: null,
/** @type {Button} */
squatButton: null,
};
},
computed: {
// ...
// 回传两个脚位
pins() {
return [this.jumpPin, this.squatPin];
},
},
watch: {
// 侦测脚位设定
pins([jumpPin, squatPin]) {
// 所有脚位都设定完成才初始化
if (!jumpPin || !squatPin) return;
this.initBtns();
},
},
created() {},
mounted() {},
beforeDestroy() {
// 清空事件
this.over();
this.jumpButton?.destroy?.();
this.squatButton?.destroy?.();
},
methods: {
// ...
/** 初始化按钮物件 */
initBtns() {
const transceiver = this.portTransceiver;
const mode = INPUT_PULLUP;
this.jumpButton = new Button({
pin: this.jumpPin,
mode,
transceiver,
}).on('rising', () => {
this.start();
this.$refs.dino.jump(); // 跳跃
});
this.squatButton = new Button({
pin: this.squatPin,
mode,
transceiver,
}).on('toggle', (value) => {
this.start();
this.$refs.dino.setSquat(value); // 蹲下
});
},
},
};
成功透过实体按钮控制小恐龙了!来挑战最高分数吧!
至此,我们成功重返侏罗纪并完成任务了,接下来往下一站迈进吧!
大家有兴趣可以自行加上更多进阶功能,例如:最高纪录、加入翼龙、不同高度的仙人掌等等。
以上程序码已同步至 GitLab,大家可以前往下载:
>>: 追求JS小姊姊系列 Day24 -- 工具人、姐妹不只身份的差别(下):从记忆体看宣告变数的可变性
OK,说好本章结要来说一下页面,那就说一下页面能干嘛,首先要提到的是有页面功能又有文章功能,同时都可...
前言 今天把昨天讲的论文做一个总结,明天就要开始介绍attention了 A2D dataset 这...
CredentialsFileView 今天就来认识 CredentialsFileView 这个工...
Agenda 资安宣言 测试环境与工具 学习目标 技术原理与程序码 References 下期预告 ...
「给你一个凭证,证明你是一个工具人。」 今天是一个户外教学。 我们要来去网站上实际把证书抓出来看看。...