Vue结合SVG开发一款可爱风射击游戏『ネコ🐱メザシ🐟アタック🌟
0x01 前言
在日站看到这么一篇有点意思的帖子,在征得原作者的同意后进行翻译转载。说实话,日本的IT软件氛围远不如国内,但是与日本其它行业一样,日本总是在走一条与众不同的路,偶尔也能给人惊喜,希望这篇文章也能给您以启发。
作者许可证
0x02 成品效果
在线体验
源码(github)
这是一款简单的触屏射击游戏。操纵着角色边进行跳跃,边发射鱼干,击中靠近的猫即可得分。
特点
- 不使用任何动画或游戏开发框架,单纯的使用vue来构建程序
- 所有的图像都以SVG制作并内嵌在JS文件中(加上vue的本体也不到100KB)
- iphone6也能顺畅的游玩
0x03 使用Vue进行游戏开发有意义吗?
Vue并不适合大型的游戏开发
就结论而言,使用vue开发复杂的动作游戏是一件吃力不讨好的事。
大量的vue组件进行响应式的刷新是相当耗性能的。目前vue在类和接口的继承及扩展方面并不容易,对于角色等高度相似的组件进行设计容易变得复杂失去控制。但是,随着Vue.js 3.0版本的不断逼近,这一现状也许会在未来得到改变。
开发迷你游戏游戏具有优势(大概)
一方面,只要是小游戏,即便是动作游戏,使用vue来开发也具有一定的优势。
-
极其轻量
※22KB的app.js包含了所有的图像
这回vue本体加上另外两个用于碰撞检测和声音播放的库,即便再加上图像(svg)也不满100KB,根本就无需『游戏加载中。。。』的画面来过渡。
-
普通的web知识可以轻松利用起来
和一般使用了canvas/webgl的框架不同,在vue的世界里,不论是游戏角色还是背景都是用普通的HTML和CSS来实现的。换句话说,我们可以使用自己熟悉的技术来解决诸如响应性,Retina支持等麻烦的问题。这对于非游戏专业的工程师和设计师来说无异是非常方便的。 -
可以进行声明式的游戏开发
使用vue进行开发的时候,我们完全可以用【声明式】的方法进行开发。
作为示例,以下是此次游戏开发的主要框架模版:- 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>
对vue稍有了解的话,我们就明白上述代码声明了:
- stage组件里包含了player、mezashi(鱼干)、cat三个组件
- player只有一个,mezashi和cat用循环指令通过mezashis和cats属性创建了多个
- player的hitCat和cat的hitMezashi用于角色之间的碰撞事件回调
当然了,这取决于游戏类型和规模。
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(鱼干)组件,使用的时候一行就可以搞定。
- 使用方.vue
<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
createjs.Tween.get(target)
.to({ x: 100, y: 100 }, 1000)
.to({ x: 200, y: 50 }, 500)
- 使用此次实现的Tween
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="...">
事件处理器部分连接起来了。
导入和播放声音
下一个难关就是声音的播放。如果是第一次接触的话,可能会有很多坑,如果了解的话就很简单了。
总的来说,你应该记住:
- 播放声音大致有以下两种方法:
Audio.play()
或者WebAudioAPI
相关的方法
这边将使用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作为超小型游戏的开发堆栈,你~值得拥有!