让前端飞Web前端之路随笔-生活工作点滴

Vue结合SVG开发一款可爱风射击游戏『ネコ🐱メザシ🐟アタック🌟

2019-07-13  本文已影响12人  Jkanon

0x01 前言

在日站看到这么一篇有点意思的帖子,在征得原作者的同意后进行翻译转载。说实话,日本的IT软件氛围远不如国内,但是与日本其它行业一样,日本总是在走一条与众不同的路,偶尔也能给人惊喜,希望这篇文章也能给您以启发。

作者许可证

0x02 成品效果


在线体验
源码(github)
这是一款简单的触屏射击游戏。操纵着角色边进行跳跃,边发射鱼干,击中靠近的猫即可得分。

特点

0x03 使用Vue进行游戏开发有意义吗?

Vue并不适合大型的游戏开发


就结论而言,使用vue开发复杂的动作游戏是一件吃力不讨好的事。
大量的vue组件进行响应式的刷新是相当耗性能的。目前vue在类和接口的继承及扩展方面并不容易,对于角色等高度相似的组件进行设计容易变得复杂失去控制。但是,随着Vue.js 3.0版本的不断逼近,这一现状也许会在未来得到改变。

开发迷你游戏游戏具有优势(大概)


一方面,只要是小游戏,即便是动作游戏,使用vue来开发也具有一定的优势。

※22KB的app.js包含了所有的图像
这回vue本体加上另外两个用于碰撞检测和声音播放的库,即便再加上图像(svg)也不满100KB,根本就无需『游戏加载中。。。』的画面来过渡。

<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>

对vue稍有了解的话,我们就明白上述代码声明了:

当然了,这取决于游戏类型和规模。

0x04 要点解说


下面我将简要介绍下开发这款游戏的具体要点。

SVG图像的制作和读取

这回的SVG我使用iPad应用Vectornator来制作。
这款应用简直就是iPad上便携版的illustrator,重要的它完全免费!天哪!
制作流程如下:用插画软件Procreate绘制草图→Vectornator进行修图并导出成SVG→最后用illustrator分解成各个部分


然后用vue来读取svg,使用的组件是svg-to-vue-component
使用此组件的优点是让你能够以vue组件而非url的方式使用SVG文件(它会在build的时候将SVG文件自动转换为Vue的组件)。由于是在build阶段进行转换的,所以你需要在vue.config.js里添加一些额外的配置(没有此文件的话请手动生成)。之后就可以和使用普通组件一般方便地用import关键字导入使用,就像下面这样:
<template>
  <mezashi-svg></mezashi-svg> <!-- 渲染导入的SVG -->
</template>
<script>
  import MezashiSvg from '@/assets/Mezashi.svg' // ※后缀一定要写
  export default {
    components: { MezashiSvg }
  }
</script>

使用之前制作(作者在另外一篇博客中介绍的)的ECont容器组件进行包裹,以此来控制图像的位置和角度。为了方便之后的碰撞检测,这边要事先设置好元素的大小和中心点。(这一点倒是有点麻烦啊)

<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(鱼干)组件,使用的时候一行就可以搞定。

<mezashi x="100" y="200" r="30"></mezashi>

接下来依样画葫芦定义好cat和player的组件。

Tween动画的组装


现在已经可以随意将角色放置在任何位置了,接下来我们来考虑动画的部分。

Tween类实现

为了更容易地实现具有高表现力的动画,我将实现Tween动画的功能。
Tween类的实现请参照/src/core/Tween.js。基本上就是在构造函数中指明目标对象,然后指定to(变化后的数值, 时间, easing函数)函数。此外,并无其他功能和公开方法。
由于许多库都已轻松地实现了Tween动画的功能,你也可以使用自己熟悉的库。我希望实现起来尽可能的轻量级,Createjs中的Tween.js那样的方法链使用起来有点麻烦,因此自己实现的了一个返回Promise对象的Tween类。

createjs.Tween.get(target)
  .to({ x: 100, y: 100 }, 1000)
  .to({ x: 200, y: 50 }, 500)
const tw = new Tween(target)
await tw.to({ x: 100, y: 100 }, 1000)
await tw.to({ x: 200, y: 50 }, 500)

这样的话,不需要在Tween中实现特殊的功能,使用普通的js语句就能够随意地控制任何关键帧。

// 一边上下摇晃一边向左移动直到离开画面
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)
}

碰撞检测


如果你决定用vue来制作一款动作游戏,恐怕碰到的第一个难题就是碰撞检测。对于面向游戏的动画框架来说,这个功能应该算是一个标配。但是在vue中就得靠我们自己实现了。
这回实现的碰撞检查实现类:/src/core/CollisionDetector.js
为了实现碰撞检测,我们需要准确地获取各个元素的坐标。通常HTMLElement.offsetTop的值并不考虑CSS的transform属性引起的变换。考虑到这种情况,我们利用Element.getBoundingClientRect()来获取元素的真实位置。

// this._comps数组存储着所有的vue组件,并以此取得真实矩形区域
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 ]
})

这个方法不受HTML的结构和滚动状态的影响,纯粹地获取元素在视口(ViewPort)中的外矩形位置。虽然不常用到,但是能够在包括IE在内的主流浏览器上运行
通过这种方法,使用定时器定期地获取Player・Cat・Mazashi的位置,并检查矩形的交集(碰撞)部分。由于此次最多只涉及几十个物体,因此如果简单地通过循环判定也应该能够平稳流畅地运行。但是我们还是决定使用主流的四叉树算法,为此引入了专门的库box-intersect

// 判定矩形是否冲突(重叠)
const result = boxIntersect(boxes).map(indexes => {
  // 由于boxIntersect返回的是冲突矩形的索引,这里转换成对应的组件
  const [i1, i2] = indexes
  return [this._comps[i1], this._comps[i2]] 
})

这样就能够获取到所有发生碰撞冲突的组件的组合。
最后,与上一次的判定结果进行比较,获取到此次新增的发生碰撞重叠的组件,并调用相应的collide方法。

const diffedRes = diffNewResults(this._lastResult, result) // 获取不同的部分,具体实现请看此文件的开头部分
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)
  }
})

顺便说下,被调用collide方法的组件会通过$emit()触发含有与之发生碰撞冲突对象名称的事件(如cat与mezashi发生了碰撞,会触发cat组件的hitMezashi事件及mezashi组件的hitCat事件),就像下面一般:

methods: {
  /* called by CollisionDetector */
  collide (targetComp, name) {
    this.$emit(`hit${name}`, targetComp)
  }
}

如此就和开头的<mezashi @hitCat="...">事件处理器部分连接起来了。

导入和播放声音


下一个难关就是声音的播放。如果是第一次接触的话,可能会有很多坑,如果了解的话就很简单了。
总的来说,你应该记住:

这边将使用WebAudioAPI,但是呢,完全自己来写是一件非常麻烦的事情,还是偷点懒引入现成的第三方库吧,我认为audio-play就非常好,同时易于使用。

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

代码非常的短,这边就全部贴出来了。系统启动的时候调用load加载读取相关的音频文件,然后在需要的时候调用playSound进行播放即可。这次需要读取的文件并不多,因此上述代码足够满足我们的需求。

部署到Firebase


这次机会难得,总想用firebase做点什么,但是鉴于时间不多,最后只是用了托管的功能。
1.在Firebase控制台新建项目
2.使用firebase init命令进行项目的初始化,这边配置仅使用hosting功能
3.从Firebase控制台中启用hosting
4.firebase deploy进行部署
        这一部分其实很简单,甚至不写出来也没什么影响,但是为了体现出firebase的简单便捷,我还是保留了这一节。

0x05 性能评价


从结果来看是非常的快速的。


0x06 结语


Vue + SVG + Firebase作为超小型游戏的开发堆栈,你~值得拥有!

上一篇下一篇

猜你喜欢

热点阅读