0x01 前言
在日站看到這么一篇有點(diǎn)意思的帖子,在征得原作者的同意后進(jìn)行翻譯轉(zhuǎn)載。說(shuō)實(shí)話,日本的IT軟件氛圍遠(yuǎn)不如國(guó)內(nèi),但是與日本其它行業(yè)一樣,日本總是在走一條與眾不同的路,偶爾也能給人驚喜,希望這篇文章也能給您以啟發(fā)。
作者許可證

0x02 成品效果

在線體驗(yàn)
源碼(github)
這是一款簡(jiǎn)單的觸屏射擊游戲。操縱著角色邊進(jìn)行跳躍,邊發(fā)射魚(yú)干,擊中靠近的貓即可得分(譯者注:其實(shí)就是給貓喂魚(yú)啦)。
特點(diǎn)
- 不使用任何動(dòng)畫(huà)或游戲開(kāi)發(fā)框架,單純的使用vue來(lái)構(gòu)建程序
- 所有的圖像都以SVG制作并內(nèi)嵌在JS文件中(加上vue的本體也不到100KB)
- iphone6也能順暢的游玩
0x03 使用Vue進(jìn)行游戲開(kāi)發(fā)有意義嗎?
Vue并不適合大型的游戲開(kāi)發(fā)
就結(jié)論而言,使用vue開(kāi)發(fā)復(fù)雜的動(dòng)作游戲是一件吃力不討好的事。
大量的vue組件進(jìn)行響應(yīng)式的刷新是相當(dāng)耗性能的。目前vue在類和接口的繼承及擴(kuò)展方面并不容易,對(duì)于角色等高度相似的組件進(jìn)行設(shè)計(jì)容易變得復(fù)雜失去控制。但是,隨著Vue.js 3.0版本的不斷逼近,這一現(xiàn)狀也許會(huì)在未來(lái)得到改變。
開(kāi)發(fā)迷你游戲游戲具有優(yōu)勢(shì)(大概)
一方面,只要是小游戲,即便是動(dòng)作游戲,使用vue來(lái)開(kāi)發(fā)也具有一定的優(yōu)勢(shì)。
-
極其輕量
※22KB的app.js包含了所有的圖像
這回vue本體加上另外兩個(gè)用于碰撞檢測(cè)和聲音播放的庫(kù),即便再加上圖像(svg)也不滿100KB,根本就無(wú)需『游戲加載中。。?!坏漠?huà)面來(lái)過(guò)渡。
-
普通的web知識(shí)可以輕松利用起來(lái)
和一般使用了canvas/webgl的框架不同,在vue的世界里,不論是游戲角色還是背景都是用普通的HTML和CSS來(lái)實(shí)現(xiàn)的。換句話說(shuō),我們可以使用自己熟悉的技術(shù)來(lái)解決諸如響應(yīng)性,Retina支持等麻煩的問(wèn)題。這對(duì)于非游戲?qū)I(yè)的工程師和設(shè)計(jì)師來(lái)說(shuō)無(wú)異是非常方便的。 -
可以進(jìn)行聲明式的游戲開(kāi)發(fā)
使用vue進(jìn)行開(kāi)發(fā)的時(shí)候,我們完全可以用【聲明式】的方法進(jìn)行開(kāi)發(fā)。
作為示例,以下是此次游戲開(kāi)發(fā)的主要框架模版:- GameStage.vue
<template>
<div class="stage-root">
<cat v-for="cat in cats" ref="cat" :key="`cat-${cat.id}`"
:x="cat.pos.x" :y="cat.pos.y" :s="cat.pos.s"
@hitMezashi="(mezashiComp) => onCatHitMezashi(cat, mezashiComp)"
@exit="removeCat(cat)"
></cat>
<mezashi v-for="mezashi in mezashis" ref="mezashi" :key="`mezashi-${mezashi.id}`"
:x="mezashi.pos.x" :y="mezashi.pos.y" :s="mezashi.pos.s"
@hitCat="(catComp) => onMezashiHitCat(mezashi, catComp)"
></mezashi>
<player ref="player"
:x="playerPos.x" :y="playerPos.y" :s="playerPos.s"
@hitCat="onPlayerHitCat"
></player>
</div>
</template>
對(duì)vue稍有了解的話,我們就明白上述代碼聲明了:
- stage組件里包含了player、mezashi(魚(yú)干)、cat三個(gè)組件
- player只有一個(gè),mezashi和cat用循環(huán)指令通過(guò)mezashis和cats屬性創(chuàng)建了多個(gè)
- player的hitCat和cat的hitMezashi用于角色之間的碰撞事件回調(diào)
當(dāng)然了,這取決于游戲類型和規(guī)模。
0x04 要點(diǎn)解說(shuō)
下面我將簡(jiǎn)要介紹下開(kāi)發(fā)這款游戲的具體要點(diǎn)。
SVG圖像的制作和讀取
這回的SVG我使用iPad應(yīng)用Vectornator來(lái)制作。
這款應(yīng)用簡(jiǎn)直就是iPad上便攜版的illustrator,重要的它完全免費(fèi)!天哪!
制作流程如下:用插畫(huà)軟件Procreate繪制草圖→Vectornator進(jìn)行修圖并導(dǎo)出成SVG→最后用illustrator分解成各個(gè)部分

然后用vue來(lái)讀取svg,使用的組件是svg-to-vue-component。
使用此組件的優(yōu)點(diǎn)是讓你能夠以vue組件而非url的方式使用SVG文件(它會(huì)在build的時(shí)候?qū)VG文件自動(dòng)轉(zhuǎn)換為Vue的組件)。由于是在build階段進(jìn)行轉(zhuǎn)換的,所以你需要在vue.config.js里添加一些額外的配置(沒(méi)有此文件的話請(qǐng)手動(dòng)生成)。之后就可以和使用普通組件一般方便地用import關(guān)鍵字導(dǎo)入使用,就像下面這樣:
<template>
<mezashi-svg></mezashi-svg> <!-- 渲染導(dǎo)入的SVG -->
</template>
<script>
import MezashiSvg from '@/assets/Mezashi.svg' // ※后綴一定要寫(xiě)
export default {
components: { MezashiSvg }
}
</script>
使用之前制作(作者在另外一篇博客中介紹的)的ECont容器組件進(jìn)行包裹,以此來(lái)控制圖像的位置和角度。為了方便之后的碰撞檢測(cè),這邊要事先設(shè)置好元素的大小和中心點(diǎn)。(這一點(diǎn)倒是有點(diǎn)麻煩?。?/p>
<template>
<e-cont :x="x - 66" :y="y - 16" :w="132" :h="32" :r="r" :s="s" :ox="66" :oy="16">
<mezashi-svg></mezashi-svg>
</e-cont>
</template>
<script>
import ECont from '@/components/core/ECont'
import MezashiSvg from '@/assets/Mezashi.svg'
export default {
name: 'Mazashi',
components: { ECont, MezashiSvg },
props: {
x: { type: [Number, String], default: 0 },
y: { type: [Number, String], default: 0 },
r: { type: [Number, String], default: 0 },
s: { type: [Number, String], default: 1 }
}
}
</script>
這樣就定義好了mezashi(魚(yú)干)組件,使用的時(shí)候一行就可以搞定。
- 使用方.vue
<mezashi x="100" y="200" r="30"></mezashi>
接下來(lái)依樣畫(huà)葫蘆定義好cat和player的組件。
Tween動(dòng)畫(huà)的組裝
現(xiàn)在已經(jīng)可以隨意將角色放置在任何位置了,接下來(lái)我們來(lái)考慮動(dòng)畫(huà)的部分。
Tween類實(shí)現(xiàn)
為了更容易地實(shí)現(xiàn)具有高表現(xiàn)力的動(dòng)畫(huà),我將實(shí)現(xiàn)Tween動(dòng)畫(huà)的功能。
Tween類的實(shí)現(xiàn)請(qǐng)參照/src/core/Tween.js。基本上就是在構(gòu)造函數(shù)中指明目標(biāo)對(duì)象,然后指定to(變化后的數(shù)值, 時(shí)間, easing函數(shù))函數(shù)。此外,并無(wú)其他功能和公開(kāi)方法。
由于許多庫(kù)都已輕松地實(shí)現(xiàn)了Tween動(dòng)畫(huà)的功能,你也可以使用自己熟悉的庫(kù)。我希望實(shí)現(xiàn)起來(lái)盡可能的輕量級(jí),Createjs中的Tween.js那樣的方法鏈?zhǔn)褂闷饋?lái)有點(diǎn)麻煩,因此自己實(shí)現(xiàn)的了一個(gè)返回Promise對(duì)象的Tween類。
- 使用CreateJS
createjs.Tween.get(target)
.to({ x: 100, y: 100 }, 1000)
.to({ x: 200, y: 50 }, 500)
- 使用此次實(shí)現(xiàn)的Tween
const tw = new Tween(target)
await tw.to({ x: 100, y: 100 }, 1000)
await tw.to({ x: 200, y: 50 }, 500)
這樣的話,不需要在Tween中實(shí)現(xiàn)特殊的功能,使用普通的js語(yǔ)句就能夠隨意地控制任何關(guān)鍵幀。
// 一邊上下?lián)u晃一邊向左移動(dòng)直到離開(kāi)畫(huà)面
const tw = new Tween(this.$data)
while (this.x > 100) {
await tw.to({ x: this.x - 100, y: this.y + (Math.random() - 0.5) * 100 }, 1000)
}
碰撞檢測(cè)
如果你決定用vue來(lái)制作一款動(dòng)作游戲,恐怕碰到的第一個(gè)難題就是碰撞檢測(cè)。對(duì)于面向游戲的動(dòng)畫(huà)框架來(lái)說(shuō),這個(gè)功能應(yīng)該算是一個(gè)標(biāo)配。但是在vue中就得靠我們自己實(shí)現(xiàn)了。
這回實(shí)現(xiàn)的碰撞檢查實(shí)現(xiàn)類:/src/core/CollisionDetector.js。
為了實(shí)現(xiàn)碰撞檢測(cè),我們需要準(zhǔn)確地獲取各個(gè)元素的坐標(biāo)。通常HTMLElement.offsetTop的值并不考慮CSS的transform屬性引起的變換??紤]到這種情況,我們利用Element.getBoundingClientRect()來(lái)獲取元素的真實(shí)位置。
// this._comps數(shù)組存儲(chǔ)著所有的vue組件,并以此取得真實(shí)矩形區(qū)域
const boxes = this._comps.map(c => {
const el = c.$el
if (!el) { return null }
const box = el.getBoundingClientRect()
return [ box.x, box.y, box.x + box.width, box.y + box.height ]
})
這個(gè)方法不受HTML的結(jié)構(gòu)和滾動(dòng)狀態(tài)的影響,純粹地獲取元素在視口(ViewPort)中的外矩形位置。雖然不常用到,但是能夠在包括IE在內(nèi)的主流瀏覽器上運(yùn)行。
通過(guò)這種方法,使用定時(shí)器定期地獲取Player?Cat?Mazashi的位置,并檢查矩形的交集(碰撞)部分。由于此次最多只涉及幾十個(gè)物體,因此如果簡(jiǎn)單地通過(guò)循環(huán)判定也應(yīng)該能夠平穩(wěn)流暢地運(yùn)行。但是我們還是決定使用主流的四叉樹(shù)算法,為此引入了專門(mén)的庫(kù)box-intersect。
// 判定矩形是否沖突(重疊)
const result = boxIntersect(boxes).map(indexes => {
// 由于boxIntersect返回的是沖突矩形的索引,這里轉(zhuǎn)換成對(duì)應(yīng)的組件
const [i1, i2] = indexes
return [this._comps[i1], this._comps[i2]]
})
這樣就能夠獲取到所有發(fā)生碰撞沖突的組件的組合。
最后,與上一次的判定結(jié)果進(jìn)行比較,獲取到此次新增的發(fā)生碰撞重疊的組件,并調(diào)用相應(yīng)的collide方法。
const diffedRes = diffNewResults(this._lastResult, result) // 獲取不同的部分,具體實(shí)現(xiàn)請(qǐng)看此文件的開(kāi)頭部分
diffedRes.forEach(pare => {
const [c1, c2] = pare
const c1Name = upperFirst(c1.$options._componentTag)
const c2Name = upperFirst(c2.$options._componentTag)
if (c1.collide) {
c1.collide(c2, c2Name, 0)
}
if (c2.collide) {
c2.collide(c1, c1Name, 1)
}
})
順便說(shuō)下,被調(diào)用collide方法的組件會(huì)通過(guò)$emit()觸發(fā)含有與之發(fā)生碰撞沖突對(duì)象名稱的事件(如cat與mezashi發(fā)生了碰撞,會(huì)觸發(fā)cat組件的hitMezashi事件及mezashi組件的hitCat事件),就像下面一般:
methods: {
/* called by CollisionDetector */
collide (targetComp, name) {
this.$emit(`hit${name}`, targetComp)
}
}
如此就和開(kāi)頭的<mezashi @hitCat="...">事件處理器部分連接起來(lái)了。
導(dǎo)入和播放聲音
下一個(gè)難關(guān)就是聲音的播放。如果是第一次接觸的話,可能會(huì)有很多坑,如果了解的話就很簡(jiǎn)單了。
總的來(lái)說(shuō),你應(yīng)該記住:
- 播放聲音大致有以下兩種方法:
Audio.play()或者WebAudioAPI相關(guān)的方法
這邊將使用WebAudioAPI,但是呢,完全自己來(lái)寫(xiě)是一件非常麻煩的事情,還是偷點(diǎn)懶引入現(xiàn)成的第三方庫(kù)吧,我認(rèn)為audio-play就非常好,同時(shí)易于使用。
import loadSnd from 'audio-loader'
import playSnd from 'audio-play'
const snds = {}
const load = name => {
loadSnd(`/snd/${name}.mp3`).then(a => { snds[name] = a })
}
load('btn')
load('catch')
load('jump')
load('gameover')
load('shot')
const playSound = name => {
const audio = snds[name]
if (!audio) {
console.warn(`No sound for: ${name}`)
return
}
playSnd(audio)
}
export default playSound
代碼非常的短,這邊就全部貼出來(lái)了。系統(tǒng)啟動(dòng)的時(shí)候調(diào)用load加載讀取相關(guān)的音頻文件,然后在需要的時(shí)候調(diào)用playSound進(jìn)行播放即可。這次需要讀取的文件并不多,因此上述代碼足夠滿足我們的需求。
部署到Firebase
這次機(jī)會(huì)難得,總想用firebase做點(diǎn)什么,但是鑒于時(shí)間不多,最后只是用了托管的功能。
1.在Firebase控制臺(tái)新建項(xiàng)目
2.使用firebase init命令進(jìn)行項(xiàng)目的初始化,這邊配置僅使用hosting功能
3.從Firebase控制臺(tái)中啟用hosting
4.firebase deploy進(jìn)行部署
????????這一部分其實(shí)很簡(jiǎn)單,甚至不寫(xiě)出來(lái)也沒(méi)什么影響,但是為了體現(xiàn)出firebase的簡(jiǎn)單便捷,我還是保留了這一節(jié)。
0x05 性能評(píng)價(jià)
從結(jié)果來(lái)看是非常的快速的。

0x06 結(jié)語(yǔ)
Vue + SVG + Firebase作為超小型游戲的開(kāi)發(fā)堆棧,你~值得擁有!
