小恐龙跑啊跑,悠闲地看着天上的云朵飘过,但是眼前忽然出现了一株仙人掌。
建立 clouds.vue
组件,负责生成云朵。
src\components\window-app-google-dino\clouds.vue
<template lang="pug">
.clouds
img.cloud(src='@/assets/google-dino/cloud.png')
</template>
<style scoped lang="sass">
@import '@/styles/quasar.variables.sass'
.clouds
position: absolute
top: 0px
left: 0px
width: 100%
height: 100%
.cloud
position: absolute
width: 100px
</style>
<script>
export default {
name: 'Clouds',
components: {},
props: {},
data() {
return {};
},
computed: {},
watch: {},
created() {},
mounted() {},
beforeDestroy() {},
methods: {},
};
</script>
在 game-scene.vue
引入 clouds.vue
。
src\components\window-app-google-dino\game-scene.vue <script>
// ...
import { mapState } from 'vuex';
import Dino from './dino.vue';
import Clouds from './clouds.vue';
export default {
name: 'GameScene',
components: {
dino: Dino,
clouds: Clouds,
},
// ...
};
src\components\window-app-google-dino\game-scene.vue <template lang="pug">
.game-scene(@click='start')
.ground
clouds(ref='clouds')
dino(ref='dino', :game-status='gameStatus')
// ...
现在应该会看到有一朵云在左上角。
接着我们希望持续生成云朵,建立相关程序。
props
gameStatus
:目前游戏状态data()
clouds
:储存已建立云朵timer
:计时器src\components\window-app-google-dino\clouds.vue <script>
/**
* @typedef {Object} Cloud 云朵
* @property {string} key
* @property {Object} style
*/
import { getRandomString } from '@/script/utils/utils';
import { random } from 'lodash-es';
export default {
name: 'Clouds',
components: {},
props: {
gameStatus: {
type: String,
default: '',
},},
data() {
return {
/** 已建立云朵
* @type {Cloud[]}
*/
clouds: [],
/** 计时器 */
timer: null,
};
},
// ...
beforeDestroy() {
this.over();
},
methods: {
/** 开始 */
start() {
// 每一秒钟产生一个云朵
this.timer = setInterval(() => {
this.addCloud();
}, 1000);
},
/** 结束 */
over() {
clearInterval(this.timer);
},
/** 建立云朵 */
addCloud() {
/** @type {Cloud} */
const cloud = {
key: getRandomString(),
style: {
top: `${random(10, 50)}%`, // 让每一朵云高度都不同
},
};
this.clouds.push(cloud);
},
},
};
.cloud
的部份加入 v-for
。
src\components\window-app-google-dino\clouds.vue <template lang="pug">
.clouds
img.cloud(
src='@/assets/google-dino/cloud.png',
v-for='(cloud, i) in clouds',
:style='cloud.style',
:key='cloud.key'
)
最後则是和小恐龙的部分一样,使用 watch
监测 gameStatus
,执行对应动作。
src\components\window-app-google-dino\clouds.vue <script>
/**
* @typedef {Object} Cloud 云朵
* @property {string} key
* @property {Object} style
*/
import utils from '@/script/utils/utils';
import { random } from 'lodash-es';
import { GameStatus } from './game-scene.vue';
export default {
name: 'Clouds',
// ...
watch: {
gameStatus(status) {
if (status === GameStatus.START) {
this.start();
return;
}
if (status === GameStatus.GAME_OVER) {
this.over();
return;
}
},
},
};
game-scene.vue
之 clouds.vue
模板的部份记得加入参数。
src\components\window-app-google-dino\game-scene.vue <template lang="pug">
.game-scene(@click='start')
.ground
clouds(ref='clouds', :game-status='gameStatus')
dino(ref='dino', :game-status='gameStatus')
// ...
尝试看看游戏开始後会不会一直出现云朵。
成功产生云朵,最後就是云朵动画与删除的部分!
云朵移动动画使用 CSS animation 达成
src\components\window-app-google-dino\game-scene.vue <style scoped lang="sass">
@import '@/styles/quasar.variables.sass'
.clouds
position: absolute
top: 0px
left: 0px
width: 100%
height: 100%
.cloud
position: absolute
width: 100px
animation: move 3.2s forwards linear
@keyframes move
0%
right: 0%
transform: translateX(100%)
100%
right: 100%
transform: translateX(0%)
尝试看看效果。
不过目前云朵移出画面之後没有删除,我们加入删除功能。
src\components\window-app-google-dino\clouds.vue <script>
/**
* @typedef {Object} Cloud 云朵
* @property {string} key
* @property {Object} style
*/
import utils from '@/script/utils/utils';
import { random } from 'lodash-es';
import { GameStatus } from './game-scene.vue';
export default {
name: 'Clouds',
// ...
methods: {
// ...
/** 删除云朵
* @param {number} index
*/
deleteCloud(index) {
this.clouds.splice(index, 1);
},
},
};
将 deleteCloud()
绑定 animationend
事件,就可以在移动动画完成後删除云朵。
src\components\window-app-google-dino\clouds.vue <template lang="pug">
.clouds
img.cloud(
src='@/assets/google-dino/cloud.png',
v-for='(cloud, i) in clouds',
:style='cloud.style',
:key='cloud.key',
@animationend='deleteCloud(i)'
)
用 Vue DevTools 检查看看有没有真的删除。
成功删除动画结束的云朵!(´,,•ω•,,)
D21 的游戏蓝图中有一项游戏逻辑:
一旦「恐龙」发生碰撞,游戏状态变为结束,画面冻结,结束游戏。
也就是当游戏结束时,云朵应该要停止动作,这点要怎麽实现呢?JS 要怎麽停止 CSS 的 animation
?
CSS 问题用 CSS 解决,这里使用 animation-play-state
达成效果。
此属性可以用来控制
animation
播放状态详细说明请见连结:MDN:animation-play-state
新增包含暂停属性的 Class
src\components\window-app-google-dino\clouds.vue <style scoped lang="sass">
// ...
.clouds
// ...
.cloud
// ...
&.pulse
animation-play-state: paused
// ...
接着透过 computed
绑定 Class。
src\components\window-app-google-dino\clouds.vue <script>
// ...
export default {
name: 'Clouds',
components: {},
// ...
computed: {
classes() {
return {
pulse: this.gameStatus === GameStatus.GAME_OVER,
};
},
},
// ...
};
src\components\window-app-google-dino\clouds.vue <template lang="pug">
.clouds
img.cloud(
// ...
:class='classes',
@animationend='deleteCloud(i)'
)
来验证看看游戏结束时,云朵会不会停止动作,回到 game-scene.vue
,让游戏状态能够变为「游戏结束」。
将 over()
绑定至滑鼠右键 click 事件。
src\components\window-app-google-dino\game-scene.vue <template lang="pug">
.game-scene(@click='start', @click.right='over')
// ...
如此一来,现在是:
来实测看看。
成功了!可以看到不只云停止,小恐龙也变成痴呆脸、提示文字也显示游戏结束。
最後就是:
「云朵」与「仙人掌」会随着分数的提高而增加速度,但不会超过最大速度。
这里需要游戏分数,所以我们透过 props
传入。
src\components\window-app-google-dino\game-scene.vue <template lang="pug">
.game-scene(@click='start', @click.right='over')
.ground
clouds(ref='clouds', :game-status='gameStatus', :score='score')
dino(ref='dino', :game-status='gameStatus')
// ...
src\components\window-app-google-dino\clouds.vue <script>
// ...
export default {
name: 'Clouds',
components: {},
props: {
gameStatus: {
type: String,
default: '',
},
score: {
type: Number,
default: 0,
},
},
// ...
};
有分数了,所以具体到底要怎麽让云朵加速呢?
老话一句,CSS 问题用 CSS 解决,这里使用 animation-duration
达成效果。
此属性可以用来控制
animation
播放长度,时间越短,云朵移动就越快,详细说明请见连结:MDN:animation-duration
实作方式为:「生成云朵的时候依照目前分数,产生动画时长,并绑定至 style 中」
这里我们在 utils.js
新增一个用来映射数值用的功能:
src\script\utils\utils.js
/** 根据区间映射数值
* @param {Number} numberIn 待计算数值
* @param {Number} inMin 输入最小值
* @param {Number} inMax 输入最大值
* @param {Number} outMin 输出最小值
* @param {Number} outMax 输出最大值
*/
export function mapNumber(numberIn, inMin, inMax, outMin, outMax) {
let number = numberIn;
if (numberIn < inMin) {
number = inMin;
}
if (numberIn > inMax) {
number = inMax;
}
const result = (number - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
return result;
}
调整 clouds.vue
中的 addCloud()
内容。
src\components\window-app-google-dino\clouds.vue <script>
// ...
export default {
name: 'Clouds',
// ...
methods: {
// ..
/** 建立云朵 */
addCloud() {
// 将分数从 0~50 映射至 3.2~0.5 之间的数值
const animationDuration = mapNumber(this.score, 0, 50, 3.2, 0.5);
/** @type {Cloud} */
const cloud = {
key: getRandomString(),
style: {
top: `${random(10, 50)}%`,
animationDuration: `${animationDuration}s`,
},
};
this.clouds.push(cloud);
},
// ...
},
};
可以看到今天云朵没有极限!─=≡Σ((( つ•̀ω•́)つ
为了让效果明显一点所以调了一个夸张一点的数值,我们改为合适一点的数值。
src\components\window-app-google-dino\clouds.vue <script>
// ...
export default {
name: 'Clouds',
// ...
methods: {
// ..
/** 建立云朵 */
addCloud() {
const animationDuration = mapNumber(this.score, 0, 1000, 3.2, 2.5);
// ...
},
// ...
},
};
建立 cactuses.vue
组件,负责生成仙人掌,基本概念与云朵相同,差在微调部分数值。
src\components\window-app-google-dino\cactuses.vue <template lang="pug">
.cactuses
img.cactus(
ref='cactus',
v-for='(cactus, i) in cactuses',
:style='cactus.style',
:key='cactus.key',
:class='classes',
src='@/assets/google-dino/cactus-1.png',
@animationend='deleteCactus(i)'
)
src\components\window-app-google-dino\cactuses.vue <style scoped lang="sass">
@import '@/styles/quasar.variables.sass'
.cactuses
position: absolute
top: 0px
left: 0px
width: 100%
height: 100%
.cactus
position: absolute
height: 70px
bottom: 30px
animation: move 2.5s forwards linear
&.pulse
animation-play-state: paused
@keyframes move
0%
right: 0%
transform: translateX(100%)
100%
right: 100%
transform: translateX(0%)
src\components\window-app-google-dino\cactuses.vue <script>
/**
* @typedef {Object} Cactus
* @property {string} key
* @property {Object} style
*/
import { getRandomString, mapNumber } from '@/script/utils/utils';
import { GameStatus } from './game-scene.vue';
export default {
name: 'Cactuses',
components: {},
props: {
gameStatus: {
type: String,
default: '',
},
score: {
type: Number,
default: 0,
},
},
data() {
return {
/** @type {Cactus[]} */
cactuses: [],
timer: null,
};
},
computed: {
classes() {
return {
pulse: this.gameStatus === GameStatus.GAME_OVER,
};
},
},
watch: {
gameStatus(status) {
if (status === GameStatus.START) {
this.start();
return;
}
if (status === GameStatus.GAME_OVER) {
this.over();
return;
}
},
},
created() {},
mounted() {},
beforeDestroy() {
this.over();
},
methods: {
start() {
this.timer = setInterval(() => {
this.addCactus();
}, 1500);
},
over() {
clearInterval(this.timer);
},
addCactus() {
const animationDuration = mapNumber(this.score, 0, 1000, 2.4, 0.6);
/** @type {Cactus} */
const cactus = {
key: getRandomString(),
style: {
animationDuration: `${animationDuration}s`,
},
};
this.cactuses.push(cactus);
},
deleteCactus(index) {
this.cactuses.splice(index, 1);
},
},
};
在 game-scene.vue
引入 cactuses.vue
。
src\components\window-app-google-dino\game-scene.vue <script>
// ...
import { mapState } from 'vuex';
import Dino from './dino.vue';
import Clouds from './clouds.vue';
import Cactuses from './cactuses.vue';
export default {
name: 'GameScene',
components: {
dino: Dino,
clouds: Clouds,
cactuses: Cactuses,
},
// ...
};
src\components\window-app-google-dino\game-scene.vue <template lang="pug">
.game-scene(@click='start')
.ground
clouds(ref='clouds', :game-status='gameStatus', :score='score')
cactuses(ref='cactuses', :game-status='gameStatus', :score='score')
dino(ref='dino', :game-status='gameStatus')
// ...
仙人掌出现惹。
仙人掌每次都一样太无趣了,加入不同的仙人掌吧!
src\components\window-app-google-dino\cactuses.vue <script>
/**
* @typedef {Object} Cactus
* @property {string} key
* @property {Object} style
* @property {string} src
*/
import { getRandomString, mapNumber } from '@/script/utils/utils';
import { sample } from 'lodash-es';
import { GameStatus } from './game-scene.vue';
import imgCactus01 from '@/assets/google-dino/cactus-1.png';
import imgCactus02 from '@/assets/google-dino/cactus-2.png';
import imgCactus03 from '@/assets/google-dino/cactus-3.png';
import imgCactus04 from '@/assets/google-dino/cactus-4.png';
export default {
name: 'Cactuses',
// ...
data() {
return {
/** @type {Cactus[]} */
cactuses: [],
timer: null,
cactusTypes: [imgCactus01, imgCactus02, imgCactus03, imgCactus04],
};
},
// ...
methods: {
// ...
addCactus() {
// 随机选择仙人掌
const src = sample(this.cactusTypes);
const animationDuration = mapNumber(this.score, 0, 1000, 2.4, 0.6);
/** @type {Cactus} */
const cactus = {
key: getRandomString(),
style: {
animationDuration: `${animationDuration}s`,
},
src,
};
this.cactuses.push(cactus);
},
// ...
},
};
src\components\window-app-google-dino\cactuses.vue <template lang="pug">
.cactuses
img.cactus(
// ...
:src='cactus.src',
@animationend='deleteCactus(i)'
)
可以看到每次出现的仙人掌都不一样了!
这时候会发现仙人掌很固定都是 1.5 秒出现一个,这样好像太简单了,跳跃时间很容易掌握,我们让每次生成的时候都加入一个随机的延迟,让每次每次生成都有时间差,增加一点难度。
src\components\window-app-google-dino\cactuses.vue <script>
/**
* @typedef {Object} Cactus
* @property {string} key
* @property {Object} style
* @property {string} src
*/
import { getRandomString, mapNumber, delay } from '@/script/utils/utils';
import { sample, random } from 'lodash-es';
// ...
export default {
name: 'Cactuses',
// ...
methods: {
start() {
this.timer = setInterval(async () => {
await delay(random(1000));
this.addCactus();
}, 1500);
},
// ...
},
};
可以发现仙人掌出现间隔出现变化了。
最後有一个小问题,游戏重新开始时,所有的角色应该要重置,而不是从上次结束的地方开始,如下图。
这里使用一个简单暴力的方法:
每次开始游戏时,都建立新的组件。ᕕ( ゚ ∀。)ᕗ
game-scene.vue
新增一个变数,随机生成字串,用於强制更新组件。
src\components\window-app-google-dino\game-scene.vue <script>
// ...
import { getRandomString } from '@/script/utils/utils';
export default {
name: 'GameScene',
// ...
data() {
return {
gameId: '',
// ...
};
},
// ...
methods: {
start() {
// ...
// 初始化变数
this.gameId = getRandomString();
this.gameStatus = GameStatus.START;
this.score = 0;
this.timeCounter = 0;
// ...
},
// ...
},
};
将 gameId
绑定为 cactuses
与 clouds
的 key
src\components\window-app-google-dino\game-scene.vue <template lang="pug">
.game-scene
.ground
clouds(
// ...
:key='"clouds-" + gameId'
)
cactuses(
// ...
:key='"cactuses-" + gameId'
)
// ...
这时候会发现一个问题,怎麽游戏开始後,云和仙人掌都没有出现了?
抓虫虫时间,透过 Vue DevTools 检查组件看看。
可以发现 timer
都是 null
,表示 start()
没有被触发,而触发 start()
的部份由 watch
负责。
让我们看看 watch
的程序码。
src\components\window-app-google-dino\clouds.vue <script>
// ...
export default {
name: 'Clouds',
// ...
watch: {
gameStatus(status) {
if (status === GameStatus.START) {
this.start();
return;
}
if (status === GameStatus.GAME_OVER) {
this.over();
return;
}
},
},
// ...
};
原因是因为组件初始化後 watch
中的 gameStatus
没有执行,所以这里我们加入 immediate
改写为:
src\components\window-app-google-dino\clouds.vue <script>
// ...
export default {
name: 'Clouds',
// ...
watch: {
gameStatus: {
handler(status) {
if (status === GameStatus.START) {
this.start();
return;
}
if (status === GameStatus.GAME_OVER) {
this.over();
return;
}
},
immediate: true,
},
},
// ...
};
cactuses.vue
中的 watch
部分也要记得改。
src\components\window-app-google-dino\cactuses.vue <script>
// ...
export default {
name: 'Cactuses',
// ...
watch: {
gameStatus: {
handler(status) {
if (status === GameStatus.START) {
this.start();
return;
}
if (status === GameStatus.GAME_OVER) {
this.over();
return;
}
},
immediate: true,
},
},
// ...
};
这样 watch
中的 gameStatus
就会在组件初次建立後执行一次。
尝试看看按右键结束游戏後,再按左键开始游戏,有没有成功清空画面(重建组件)。
清场成功!
目前为止我们成功让所有角色都登场了,最後就是加入完整游戏逻辑了!✧*。٩(ˊᗜˋ*)و✧*。
以上程序码已同步至 GitLab,大家可以前往下载:
<<: Day.29 「Class 语法糖~」 —— ES6 Class 构造函式
在怀旧主机中、色差端子通常是除了 HDMI 以外能用以得到最佳解析度的接头、若主机仍支援这个端子的话...
Q: 终於要讲效能了! A: 以Loading为范例讲黑~ Animation Loading 直...
如果你也有跟着教程做的话,第10节有个练习,可以来跟我交流一下答案,我也不知道我的写法是不是好的,但...
半夜睡不着来更新XD Python属於「物件导向程序语言」(Object-oriented prog...
我们前面提到过深度学习就是模仿神经网路建构一个庞大的训练模型,来达到特徵的选取(调整 weight...