游戏地址: https://heyongsheng.github.io...
开发语言:vue
运行平台:Chrome
gitee地址:https://gitee.com/ihope_top/n...
github地址:https://github.com/heyongshen...
游戏已开源,欢送大家体验,也能够自行批改用作公司年会游戏等

前言

各位掘金的xdm,又是一年新春到,在这里提前给各位兄弟们拜年了,祝大家身体健康,万事如意。明天这篇文章呢,是为了掘金新春征文诞生的,这里特意给大家写了一个小游戏,所谓技术不够创意来凑,尽管游戏用到的技术都是很个别很简略的,然而也让我筹备了不少的工夫,小游戏全副由本人实现,网上拼凑的资源,美术、音效可能都不完满,大家将就将就哈,心愿大家可能喜爱,强烈建议大家在阅读文章之前先点击游戏链接https://heyongsheng.github.io... 前去体验两把。

那上面咱们就正式的来开始游戏开发的解说了。

小游戏内容较多,不重要的中央会一笔带过或者省略,如果有人对游戏中没有提到的技术感兴趣,能够在评论区提出,后续能够针对性的出文章解说,另外文中代码仅张贴要害局部代码,如需查看残缺代码,请移步gitee或者GitHub。

游戏规则

玩家须要按住炮竹进行左右挪动来攻打年兽,屏幕两头会定时呈现问题,答复对问题会减少攻击力等,每道题的答复工夫为8秒钟,问题呈现的距离为5秒钟,年兽血量为0时游戏完结,击败年兽用时越少越牛逼。

菜单及全局配置

全局配置

setting: {  isPlay: false,  showBulletChat: true}

全局配置其实就俩,声音管制和弹幕管制,因为经测试,游戏在性能非常不好的机器会卡顿,所有给出了是否显示弹幕的管制,至于弹幕大小、色彩、密度这些因为工夫关系就没有写。至于声音管制,那必定是必须的,一是因为避免忽然播放音乐对用户造成影响,二是浏览器也有限度,禁止声音自动播放。

菜单

布局方面就不说了,这里简略的说一下我菜单生成时的思路,因为给菜单增加鼠标滑过和点击的音效,所以用v-for循环数据的办法比拟好,要不然鼠标事件就要写好几遍。具体的代码如下

<div class="menu-box">  <div    class="menu-item"    v-for="(item, index) in menuList"    :key="index"    @mouseover="$store.commit('playAudio', hoverMusic)"    @click="$store.commit('playAudio', clickMusic),item.clickHandle()"    v-show="item.show()"  >    {{item.name}}  </div></div>
// 节选menuList: [    {      name: '开始游戏',      clickHandle: () => {        this.gameBegin()      },      show: () => true    },    {      name: '关上声音(强烈建议)',      clickHandle: () => {        this.$store.commit('tooglePlay', true)      },      show: () => !this.$store.state.setting.isPlay    },    {      name: '敞开声音',      clickHandle: () => {        this.$store.commit('tooglePlay', false)      },      show: () => this.$store.state.setting.isPlay    }  ],

菜单的每一项次要有三个属性,名称、点击事件和管制显示,因为有些菜单项须要依据理论状况决定是否显示,比方关上声音和敞开声音,须要依据以后声音是否关上来判断谁显示谁暗藏,如果咱们定义数据的时候间接把管制声音的变量赋值给show,那么后续声音变动的时候,show是不会动静更新的,这里咱们咱们赋值给show一个函数,就能够达到冬天更新的目标了。

声音

游戏没有声音怎么行,这里援用的音乐是序曲,哈哈哈,是不是一下子就有年味了。游戏中的声音次要有两个类型,一种是长时间播放,须要管制播放暂停的,比方背景音乐,另一种是即时性的,比方菜单滑动声、子弹撞击声等,所以背景音乐的实例咱们须要存储下来,而即时音效随用随建就行,我这里偷了个懒,没有写独自的声音配置文件,间接写vuex里了。

背景音乐

window.backMusic = new Audio()window.backMusic.src = require('../assets/mp3/back.mp3')window.backMusic.loop = truewindow.backMusic.load()window.backMusic.currentTime = 127.2 // 背景音乐默认定位到舒缓片段

这样咱们在任何中央管制播放间接调用或更改window.backMusic就行了。

即时音效

playAudio (state, src) {  if (state.setting.isPlay) {    const audio = new Audio()    audio.src = src    audio.load()    audio.volume = .5    audio.play()  }}

这里播放音效的时候须要判断以后的声音开关是否关上,如果关上的话在进行播放,留神,这里不能通过给繁多的audio对象扭转地址的形式播放不同的音效,因为如果在以后声音正在播放时候,批改音效地址会报错。

弹幕

这个创意是我在听春节序曲的背景音乐时想到的,因为一听这个就想到春晚,想到短片中全国各地的人民送祝福,于是我就想把这个加进来,联合背景音乐,是不是一下子感觉就来了。也心愿大家能够送上本人的祝愿,我也会把你的祝愿更新到弹幕里的。这里的弹幕就只为了满足游戏的需要,不会太简单。

首先,咱们须要梳理一下弹幕的需要和留神点

  • 弹幕横向和纵向不能重叠
  • 两天弹幕之间的距离最好能够随机
  • 弹幕超出屏幕要主动移除

首先说弹幕不能重叠的问题,弹幕纵向不能重叠的话,咱们就须要有一个弹道的概念,也就是让每一条弹幕都有本人的轨道,各走各的,当然就不会重叠了。我这里是依据屏幕高度,分成了10个弹道,原本打算屏幕越大,弹道越多的,然而思考到性能问题,就采纳了这种计划。

其次就是弹幕横向的避免重叠,我百度的时候看到其余作者提到的追及问题什么的,奈何我是个学渣,没有看太明确,于是就本人想了解决办法,咱们这里每条弹幕的挪动速度是一样的,那须要思考的就是每条弹幕呈现的机会问题了,咱们须要在同一弹道的前一条弹幕齐全呈现后,再生成下一条弹幕,两头能够加一个咱们规定好范畴的随机间隔,这样更好看一点。

上面来看一下代码怎么实现的。

ballistic: 0, // 弹道数量bulletSpeed: 2, // 弹幕速度bulletInterval: [300, 500], // 弹幕距离screenWidth: document.documentElement.clientWidth, // 屏幕宽度screenHeight: document.documentElement.clientHeight, // 屏幕高度
/** * @description: 展现弹幕 * @param {*} * @return {*} */showBullet () {  // 此处间接设定了10条弹道,也可依据屏幕高度和弹幕高度计算弹道数  let ballisticArr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]  // 按随机程序在所有的弹道增加弹幕  let ballisticLaunch = () => {    let randomIndex = Math.floor(Math.random() * ballisticArr.length)    let ballisticIndex = ballisticArr.splice(randomIndex, 1)[0]    this.createBullet(ballisticIndex)    if (ballisticArr.length > 0) {      setTimeout(ballisticLaunch, Math.random() * 1000)    }  }  ballisticLaunch()  // this.createBullet(2)},

我这里的办法是先设定好弹道数,而后把这些的弹道的序号放进一个数组,开始时间接从这个数组去取编号,往这个弹道放进去一个弹幕,而后循环,直到每一条弹道都被用完为止,那么问题来了,这时候咱们每条弹道只有一条弹幕,怎么生成后续弹幕呢,这里的思路是在每一条弹幕挪动的时候,判断本人的挪动间隔,当达到适合的间隔时(本身齐全呈现在屏幕中并且间隔屏幕右侧达到了咱们设定的两条弹幕间的间隔)就调用加载下一条弹幕的办法,并把本身的弹道编码传入,加上咱们这里弹幕是匀速的,就不会有重叠的问题了。

/** * @description: 增加弹幕 * @param {*} index 弹道索引 * @return {*} */createBullet (index) {  let bullet = document.createElement('div')  let bulletHeight = document.documentElement.clientHeight / 10  bullet.className = 'bullet-chat'  bullet.style.left = this.screenWidth + 'px'  bullet.style.top = index * bulletHeight + 'px'  bullet.createNext = false // 是否已创立下一个弹幕  bullet.nextSpace = Math.random() * (this.bulletInterval[1] - this.bulletInterval[0]) + this.bulletInterval[0] // 下一个弹幕距离  // 从弹幕库随机取弹幕  let dataLength = this.blessingData.length  let randomIndex = Math.floor(Math.random() * dataLength)  let blessing = this.blessingData[randomIndex]  bullet.innerText = blessing.name + ":" + blessing.value  this.$refs.bulletChat.appendChild(bullet)  // 弹幕挪动  let bulletMove = () => {    bullet.style.left = bullet.offsetLeft - this.bulletSpeed + 'px'    if (!bullet.createNext) {      // 如果弹幕间隔屏幕右侧间隔超出弹幕距离,则加载下一条弹幕      if (bullet.offsetLeft < (this.screenWidth - bullet.offsetWidth - bullet.nextSpace)) {        this.createBullet(index)        bullet.createNext = true      }    }    // 如果弹幕间隔右侧间隔大于等于屏幕宽度,则移除弹幕    if (bullet.offsetLeft < (-bullet.offsetWidth)) {      this.$refs.bulletChat.removeChild(bullet)    } else {      requestAnimationFrame(bulletMove)    }  }  bulletMove()}

这里咱们引入了一个弹幕库,每次从中随机取一条,这样就防止旧弹幕无奈被看到的问题了,另外大家也都看到了,这里用的定时办法是requestAnimationFrame,这个真的比setinterval要好,本我的项目根本所有用到动画的中央都用的这个,也倡议大家都用这个办法代替setinterval,益处比拟多,这里就不占字数了,大家感兴趣自行百度吧。

年兽

这个可恶的小东西就是咱们的年兽了,年兽的组成很简略,一个小图标,加一个血量,而后咱们让它来回动起来就能够了。当血量为0时候咱们就让它隐没。

<!-- 年兽 --><div  class="nianshou"  :style="'marginLeft:' + nianshouLeft + 'px'"  v-show="nianshouHP">  <p>HP: {{ nianshouHP }}</p>  <img src="../assets/nianshou.png" class="nianshou-img" /></div>
nianshouLeft: 0, // 年兽间隔右边的间隔nianshouMove () {  // 更新游戏工夫  this.gameDuration = new Date().getTime() - this.gameBeginTime  if (this.nianshouLeft + 200 >= this.screenWidth) {    this.nianshouMoveDir = -4  } else if (this.nianshouLeft < 0) {    this.nianshouMoveDir = 4  }  this.nianshouLeft += this.nianshouMoveDir  this.nianshouInterval = requestAnimationFrame(this.nianshouMove)},

咱们的游戏规则是用时越少越厉害,所以咱们须要计算游戏用时多少,这里咱们以年兽开始挪动时为游戏开始工夫,另外咱们还须要在年兽撞墙的时候往反方向静止,所以这里咱们判断了年兽间隔屏幕右边和左边的间隔,一旦达到界定值的时候,则扭转挪动方向,也就是扭转挪动值的正负

炮竹

这个小玩意儿就是咱们的炮竹了,也相当于咱们的武器,我原本想找一个烟花筒来开释烟花的,奈何资源无限,就用这个将就吧。这个小炮竹会一直的收回光束去打年兽,这里对于炮竹,就是鼠标按下的时候增加挪动事件,让他左右挪动就能够了。

第一步必定就是炮竹的挪动,这个咱们不做的太简单,间接让鼠标拖动进行左右挪动就行了,不让高低挪动是为了你举着炮竹今年兽脸上怼。

思路,鼠标点击炮竹,给整个区域增加挪动事件,不给炮竹增加挪动事件时因为鼠标挪动过快的话很容易超出炮竹的范畴,造成不好的游戏体验,当鼠标抬起时,咱们再把这个事件给移除。至于挪动,咱们须要先定义一个clientx,每次鼠标挪动的时候存储鼠标间隔屏幕左侧的间隔,当鼠标再次挪动的时候,咱们用以后光标间隔左侧的间隔倡议刚刚存储的,就能够得出鼠标挪动的间隔,而后咱们把这个值的变动赋值给炮竹的margin-left

<!-- 鞭炮 --><div  class="paozhu"  ref="paozhu"  @mousedown="addMove"  :style="'marginLeft:' + paozhuLeft + 'px'">  <img src="../assets/paozhu.png" alt="" /></div>
clientX: 0, // 鼠标上次的地位paozhuLeft: 0 // 炮竹间隔右边的间隔// 鼠标按下,增加挪动事件addMove (e) {  e = e || window.event  this.clientX = e.clientX  this.clientY = e.clientY  this.$refs.gemeWrap.onmousemove = this.moveFunc},// 鼠标拖动,挪动炮竹moveFunc (e) {  e = e || window.event  e.preventDefault()  let movementX = e.clientX - this.clientX  this.paozhuLeft += movementX  this.clientX = e.clientX},// 鼠标抬起,移除挪动事件removeMove () {  this.$refs.gemeWrap.onmousemove = null},

子弹

咱们暂且称炮竹收回的光束为子弹吧,子弹的实现原理很简略,定时发射子弹,发射子弹时获取炮竹的横向坐标,再以屏幕高度减去炮竹高度为纵向坐标,生成之后让子弹往上跑就行了,当子弹间隔顶部间隔小于等于年兽的高度时,判断子弹的横向坐标是否和年兽的横向坐标重合,如果重合就对年兽扣血,播放击中音效,移除子弹,如果未重合,则在子弹跑出屏幕时移除子弹。

这里咱们设置了一个子弹飞行速度,如果你玩过了游戏,肯定会发现,刚开始不好射中吧,哈哈哈,这也算是减少了难度,当然,如果答对了问题,射速,攻速,挫伤都会相应的减少。

createBulletInterval: null, // 创立子弹的定时器frequency: 5, // 发射子弹频率bulletSpeed: 10, // 子弹飞行速度damage: 2,// 子弹攻击力lastBulletTime: 0, // 上次发射子弹工夫// 生成子弹createBullet () {  // 子弹  let now = new Date().getTime()  if (now - this.lastBulletTime > (1000 / this.frequency)) {    let bullet = document.createElement('div')    bullet.className = 'bullet'    bullet.style.left = this.paozhuLeft + 25 + 'px'    bullet.style.top = this.screenHeight - 123 + 'px'    this.$refs.gemeWrap.appendChild(bullet)    this.$store.commit('playAudio', require('../assets/mp3/emit.mp3'))    // 子弹挪动    let bulletMove = () => {      bullet.style.top = bullet.offsetTop - this.bulletSpeed + 'px'      // 如果子弹间隔顶部的间隔为年兽的高度时,判断子弹和年兽的程度地位是否重合      if (bullet.offsetTop <= 250 && bullet.offsetLeft >= this.nianshouLeft && bullet.offsetLeft <= this.nianshouLeft + 200) {        // 年兽掉血        this.nianshouHP -= this.damage        this.$store.commit('playAudio', require('../assets/mp3/boom.wav'))        if (this.nianshouHP <= 0) {          this.nianshouHP = 0          this.gameOver()        }        // 子弹隐没        this.$refs.gemeWrap.removeChild(bullet)        // cancelAnimationFrame(bulletMove)      } else if (bullet.offsetTop <= 0) {        this.$refs.gemeWrap.removeChild(bullet)        // cancelAnimationFrame(bulletMove)      } else {        requestAnimationFrame(bulletMove)      }    }    bulletMove()    this.lastBulletTime = now  }  this.createBulletInterval = requestAnimationFrame(this.createBullet)}

因为requestAnimationFrame不能设置间隔时间,所以这里咱们就在生成子弹的时候记录下生成子弹的工夫,在requestAnimationFrame下一次运行的时候,判断工夫距离是否满足咱们对子弹频率的要求,如果满足则往下执行,如果不满足跳过本次执行。

问题

本游戏的一大特色,就是退出了答题零碎,否则始终在那biubiubiu的打怪兽有啥意思呢,年兽的血量为2021,靠初始攻速和挫伤得打半天,如果答对问题,则会减少buff,打年兽能力蹭蹭的往上涨。

首先来剖析一下问题的需要

  • 每道题的答题工夫是8秒钟,无论是否提前抉择均展现8秒
  • 答对题目则减少buff
  • 答错或者在倒计时完结未抉择答案将展现正确答案
  • 每道题的间隔时间是5秒钟
  • 每次出题从题库随机取题,呈现过的题目不会第二次抽取

先从最简略的开始,从题库抽取题目

questionJson: require('@/assets/data/question.json'), //问题源数据questionData: [], // 本轮游戏题库questionList: [],// 问题列表let dataLength = this.questionData.lengthlet randomIndex = Math.floor(Math.random() * dataLength)let question = this.questionData.splice(randomIndex, 1)[0]

很简略,接下来就是增加倒计时,先加的是题目距离倒计时,在一道题目被增加时候,展现5秒钟倒计时,而后展现题目并开始答题倒计时

// 增加展现倒计时  let showCountDown = () => {    data.showTime--    if (data.showTime > 0) {      setTimeout(showCountDown, 1000)    } else {      // 倒计时完结,展现问题并开始答题倒计时      answerCountDown()    }  }

接下来是答题倒计时,游戏设置的题目是5道,每道题完结会先判断用户是否作答,如果没有作答,主动将后果设置为谬误答案,之后再判断题目是否达到5道,如果没有达到则持续增加,直到够5道为止。

// 增加答复倒计时  let answerCountDown = () => {    data.answerTime--    if (data.answerTime > 0) {      setTimeout(() => {        showCountDown()      }, 1000)    } else {      // 倒计时完结,如果没有抉择正确答案,则增加一道谬误答案      if (!data.result) {        data.result = '2021'      }      // 如果问题有余5道,则增加一道问题      if (this.questionList.length < 5) {        this.addQuestion()      }    }  }

在接下来就是答题了,先来看一下问题面板的dom构造

<!-- 问题面板 -->  <div    class="question-panel panel-item"    :class="{ clientCenter: question.answerTime > 0 }"    v-for="(question, index) in questionList"    :key="index"  >    <p class="show-count-down" v-if="question.showTime > 0">      {{ question.showTime }}    </p>    <div class="question-wrap" v-else>      <div class="count-down" v-if="question.answerTime > 0">        <p>请在{{ question.answerTime }}秒内点击正确答案</p>      </div>      <div class="question-panel-title">问题 {{ index + 1 }}</div>      <div class="question-container">        <div class="question-title">{{ question.question.title }}</div>        <div class="answer-wrap show" v-if="!question.result">          <div            class="answer-item"            v-for="item in question.question.option"            :key="item.key"            @mouseover="$store.commit('playAudio', hoverMusic)"            @click="answerQuestion(item.key, question)"          >            {{ item.key }}:{{ item.value }}          </div>        </div>        <div class="answer-wrap result" v-else>          <div            class="answer-item"            v-for="item in question.question.option"            :key="item.key"            :class="{              result: question.question.answer === item.key,            }"          >            {{ item.key }}:{{ item.value }}            <span class="check" v-if="question.result === item.key">◇</span>          </div>        </div>        <div          class="buff"          v-if="question.result === question.question.answer"        >          攻速+1 射速+1 挫伤+1        </div>        <div          class="desc"          v-if="            question.result && question.result !== question.question.answer          "        >          {{ question.question.desc }}        </div>      </div>    </div>  </div>

再看一下题库中题目的构造

  {    "title": "以下哪位是神舟十三号航天员?",    "option": [      {        "key": "A",        "value": "翟志刚"      },      {        "key": "B",        "value": "刘伯明"      },      {        "key": "C",        "value": "聂海胜"      }    ],    "answer": "A",    "desc": "神舟十三号航天员是翟志刚、王亚平、叶光富"  },

联合咱们下面倒计时,答复等,一个问题的残缺构造应该是上面这样

{    question: question, // 题库中的题目    answerTime: 9, // 答复倒计时,    showTime: 6, // 展现倒计时    result: null, // 用户答复的答案}

这么一看就好办多了,咱们只须要再点击选项的时候,把选项的值赋值给result就行了,而后依据result的值判断用户是否答题,是否答对。

这里在最外层的dom构造上,有这样一行代码

:class="{ clientCenter: question.answerTime > 0 }"

这个判断答题倒计时是否完结,如果没有完结,则展现在屏幕最地方,不便用户查看和抉择,曾经完结,则展现在屏幕左侧,方面用户查看和分享。

游戏完结

游戏完结将展现游戏问题,并从用户祝愿中随机抽取一条进行展现

到这里整个游戏就实现了,因为篇幅无限,的确无奈将每一个细节解说具体,如果有敌人对哪里有问题,欢送在评论区进行发问或者返回github或者码云提issue,在这里提前给各位拜年了!祝大家工作顺利,身体健康,全家和和美美,万事如意!