Vue結(jié)合SVG開(kāi)發(fā)一款可愛(ài)風(fēng)射擊游戲『ネコ??メザシ??アタック??』

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ā)堆棧,你~值得擁有!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前幾日聽(tīng)到一句生猛與激勵(lì)并存,可怕與尷尬同在,最無(wú)奈也無(wú)解的話:“90后,你的中年危機(jī)已經(jīng)殺到”。這令我很受觸動(dòng)。...
    王鈺峰閱讀 4,621評(píng)論 1 22
  • UI組件 element- 餓了么出品的Vue2的web UI工具套件 Vux- 基于Vue和WeUI的組件庫(kù) m...
    柴東啊閱讀 15,963評(píng)論 2 140
  • 簡(jiǎn)說(shuō)Vue (組件庫(kù)) https://github.com/ElemeFE/element" 餓了么出品的VUE...
    Estrus丶閱讀 1,908評(píng)論 0 1
  • 時(shí)下正是杏子黃熟時(shí)節(jié),看著滿樹(shù)橙黃的杏子,在綠葉的襯托下顯得更加鮮艷剔透,不經(jīng)意望一眼,忍不住流口水的記憶,...
    沐源工作室閱讀 1,036評(píng)論 11 9
  • 2018立春過(guò)后是農(nóng)歷戊戌年。按中醫(yī)五行來(lái)說(shuō),今年火運(yùn)過(guò)旺。心屬火,心臟不好的人要格外注意??梢钥匆幌率滞笱?,如...
    唔邪閱讀 461評(píng)論 0 1

友情鏈接更多精彩內(nèi)容