美文网首页让前端飞Web前端之路随笔-生活工作点滴
Vue结合SVG开发一款可爱风射击游戏『ネコ🐱メザシ🐟アタック🌟

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

作者: Jkanon | 来源:发表于2019-07-13 14:35 被阅读12次

    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作为超小型游戏的开发堆栈,你~值得拥有!

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/qgxhkctx.html