前端写的跑酷游戏——《奔跑吧!程序员》js小游戏火热来袭,快来一起奔跑吧

20,017 阅读7分钟

我正在参加「初夏创意投稿大赛」详情请看:初夏创意投稿大赛

在线体验地址:summer.pkec.net/
源码地址:gitee.com/ihope_top/j…

最新文章推荐 electron+vue从0到1实现一个桌面端日期时间倒计时软件实践 juejin.cn/post/710964…

前言

不知不觉夏天又到了,提到夏天你们能想到什么?空调、西瓜还有冰淇淋?但是夏天不止有这些,还有运动、流汗、露身材,还记得每年夏天的运动会吗?还记得那年夏天的天天酷跑吗?今天我就用js来给大家带来一个跑酷小游戏——《奔跑吧!程序员》,希望大家可以喜欢

游戏介绍

特色功能

特色功能涉及后端接口支撑,故仅在活动期间生效,征文活动结束后可能会失效,但游戏仍可正常访问,如遭遇恶意攻击或者其他不可控因素,此功能可能会提前失效。

成绩上传

image.png

没错,这次我们打通了前后端,支持成绩上传了,在游戏结束后,会出现成绩上传的选项,您只需要填入自己的掘金ID就可以上传本次成绩,上传掘金ID的主要目的是防止用户随机填入非法昵称,所以这里需要根据用户的掘金id获取用户的掘金昵称。

小游戏仅供娱乐,所以没有做防作弊措施,大家千万不要作弊哦,但是为了防止误操作,所以还是限制了同样的成绩不能重复上传,就是所两次都跑了500米,第二次的成绩不能上传,如果第一次跑了500米,第二次跑了100米,那么两次成绩都可以上传

掘金ID获取方法:

image.png

进入个人主页,地址栏后面的数字就是你的id

全员累计奔跑

image.png

游戏结束后用户上传的成绩会被存入数据库,首页会显示所有用户上传的成绩总和

排行榜

image.png

首页会展示排行榜入口,排行榜会展示所有用户的单次进程排行,一个用户可多次上榜,如张三第一次跑了一千米,第二次跑了两千米,其他用户的最高成绩为800米,那么第一名和第二名都是张三。

规则介绍

image.png

开始游戏后,人物会自动向前奔跑,奔跑的图中会遇到小恶魔,用户必须躲避小恶魔继续向前奔跑,如碰到小恶魔,则游戏结束。

操作方式:

跳跃:按 w 键或 键进行跳跃躲避下方的小恶魔

image.png

下滑:按 s 键或 键进行下滑躲避上方的小恶魔

image.png

随着里程的增加,人物奔跑的速度会越来越快,小恶魔的数量也会越来越多(有上限),规则介绍就到这里啦,快去体验一下游戏吧。

游戏开发

场景开发

白云开发

image.png

单个的白云其实就是一个圆角矩形,然后用伪类元素做两个圆叠加起来形成的,代码如下

.cloud-item {
  position: absolute;
  width: 175px;
  height: 55px;
  margin: 50px;
  border-radius: 100px;
  background: #fff;
}
.cloud-item::before, .cloud-item::after {
  content: '';
  display: block;
  background: #fff;
  position: absolute;
}
.cloud-item::before {
  content: '';
  display: block;
  width: 100px;
  height: 100px;
  border-radius: 50%;
  top: -90%;
  right: 10%;
}
.cloud-item::after {
  content: '';
  display: block;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  top: -54%;
  left: 14%;
  transform: rotate(-25deg);
}

下面就是随机生成白云,之后给白云一个从右往左移动的动画,为了生成的白云更符合观感,我们给它一个随机的大小,然后根据这个大小再来一个对应的移动速度,就变成了下面这样

2.gif

附上代码

<div class="cloud-wrap" ref="cloudWrap"></div>
screenWidth: document.documentElement.clientWidth,
lastCreateTime: 0,
cloudFrequency: 10,
cloudSpeed: 1

// 生成云朵
createCloud() {
  let now = new Date().getTime()
  if (now - this.lastCreateTime > 3000) {
    // 创建云朵
    let cloudItem = document.createElement('div')
    cloudItem.className = 'cloud-item'
    // 设置云朵变化系数
    cloudItem.cloudScale = Math.random()
    // 设置云朵大小
    cloudItem.style.transform = 'scale('+cloudItem.cloudScale+')'
    // 设置云朵透明度
    cloudItem.style.opacity = cloudItem.cloudScale
    // 设置云朵位置
    let _left = this.screenWidth
    cloudItem.style.left = _left + 'px'
    let _top = Math.random() * 400
    cloudItem.style.top = _top + 'px'

    this.$refs.cloudWrap.appendChild(cloudItem)
    // 云朵移动
    let cloudMove = () => {
      // 云朵越大,移动速度越快
      let moveX = this.cloudSpeed * cloudItem.cloudScale
      let _left = +cloudItem.style.left.slice(0, -2)
      cloudItem.style.left = _left - moveX + 'px'

      // 如果云朵距离屏幕顶部距离大于等于屏幕高度,则移除此云朵
      if (cloudItem.offsetLeft < (-cloudItem.offsetWidth)) {
        this.$refs.cloudWrap.removeChild(cloudItem)
      } else {
        requestAnimationFrame(cloudMove)
      }
    }
    cloudMove()
    cloudMove()
    this.lastCreateTime = now
  }
  requestAnimationFrame(this.createCloud)
},

下面是代码块版本 code.juejin.cn/pen/7103402…

地面开发

ground.png

地面部分就是用这样的小方块平铺形成的,如果高度不够的话,就再来一个颜色相近的背景色

<!-- 地面 -->
<div class="ground-wrap"></div>
.ground-wrap {
  height: 150px;
  box-sizing: border-box;
  background: url('./assets/images/ground.png') #685166;
  background-repeat: repeat-x;
  background-size: 50px;
}

我们都知道,游戏里的人物运动其实并不是人物本身在动,而是场景的移动衬托出人物在运动,这里我们需要地面也在不断的运动,衬托出人物在运动。由于我们这里使用的是背景图片,所以只需要控制背景图片的定位即可

因为随着游戏的进行,人物会移动的越来越快,所以这里地面的移动速度也需要随着speed(全局速度控制变量)变化,另外我们对于人物的奔跑距离也在这里进行计算,当人物奔跑到一定距离时改变速度

 // 地面背景横向滚动
    groundScroll() {
      let ground = document.querySelector('.ground-wrap');
      let _left = 0
      
      ground.style.backgroundPositionX = _left +'px';
      let cityMove = () => {
        if (_left <= -600) {
          _left = 0
        }
        _left -= this.speed * 3
        this.total += (this.speed / 10)
        if (this.total >= 40000) {
          this.speed = 6
        } else if (this.total >= 4000) {
          this.speed = 5.5
        } else if (this.total >= 2000) {
          this.speed = 5
        } else if (this.total >= 1000) {
          this.speed = 4.5
        } else if (this.total >= 500) {
          this.speed = 4
        } else if (this.total >= 200) {
          this.speed = 3.5
        }
        ground.style.backgroundPositionX = _left +'px';
        this.groundMoveInterval = requestAnimationFrame(cityMove)
      }
      cityMove()
    },

人物开发

image.png

人物跑动其实就是来回切换这样的几张静态图片,之所以没有用gif,是因为我还要控制人物跑动的速度,gif我没找到怎么控制速度的,我们先来看一下不同速度的跑动动画

3.gif

4.gif

下面是代码

先加载一下跑动的图片数组

for (let index = 0; index < 10; index++) {
  let img = require('@/assets/images/user/run/Run_00'+index+'.png')
  this.userRunList.push(img)
}

跑步时切换图片

// 动画
run() {
  let _this = this
  let _index = 0

  let _run = () => {
    let now = new Date().getTime()
    if (now - this.lastRunTime > 120 - (this.speed * 20)) {
      if (_index > (this.userRunList.length - 1) ) {
        _index = 0
      }
      _this.userPic = this.userRunList[_index]
      _index++
      this.lastRunTime = now
    }
    this.runInterval = requestAnimationFrame(_run)
  }
  _run()
},

不同于跑步的动画处理,跳跃和下滑我都没有做动画处理,一是因为它的时间不好控制,二是太多的图片也会导致,所以这里我们就用两张静态图片,展示两种状态

slide () {
  this.userPic = this.userSlidePic
},
jump () {
  this.userPic = this.userJumpPic
}

障碍物开发

这种飞行物的开发其实原理都一样,从我最开始的年兽大作战中的弹幕到抗疫的汤圆中的柱子,又或者是刚才说过的天空中的云朵,都一样,都是按规定的时间生成一个物体,然后给它一个移动的动画。

这里不一样的地方是每个障碍物里包含的小恶魔数量不一样,障碍物可能在上面也可能在下面。

针对数量这个问题,我是根据speed(全局速度变量)来随机生成的,最少一个,最多四个,数量越多,当然难度也就越大。

针对上下这个问题,无非就是写一个靠上和靠下的样式,然后生成的时候随机进行生成,赋予相应的样式,注意,这里需要将这个状态保存下来,因为进行碰撞检测的时候要用。

image.png

// 生成障碍物
    createObstruction () {
      let obsList = document.createElement('div')
      // 让障碍物随机在上方或者下方
      let state = Math.random() > 0.5 ? 'top' : 'bottom'
      obsList.className = 'obs-list-' + state
      obsList.state = state
      // 根据速度等级,随机生成相应数量的小恶魔
      let random = Math.ceil(Math.random() * (this.speed - 2))
      for (let index = 0; index < random; index++) {
        let obsItem = document.createElement('div')
        obsItem.className = 'obs-item'
        obsList.appendChild(obsItem)
      }
      obsList.style.left = this.screenWidth + 'px'
      // obsList.createNext = false // 是否已创建下一个障碍物
      obsList.nextSpace = Math.random() * (this.obsInterval[1] - this.obsInterval[0]) + this.obsInterval[0] // 下一个障碍物间隔

      this.$refs.obstructionWrap.appendChild(obsList)

    },

这里障碍物的生成还借鉴了弹幕那里的生成方案,那就是一个障碍物出来多久后,自动加载下一个,而不是定时进行创建,这里有点忘了当初怎么想的了,想起来再补充吧。

之后就是障碍物的移动,这里障碍物的移动速度设置的和地面是一样的,这样有一种障碍物是漂浮在地面上的感觉,另外和白云不一样的是这里障碍物的移动是控制的整体障碍物的移动,而不是给单一障碍物添加的移动动画,原因是因为这里的障碍物不需要给每个障碍物不同的移动速度,另一个原因是我们需要在障碍物碰到人的时候停止所有障碍物的移动,如果是给单一障碍物添加移动动画,显然是很难达到这个需求的。

  // 获取所有障碍物
  let obsDoms = this.$refs.obstructionWrap.children
  let obsList = Array.from(obsDoms)

  let nextItem = null

  // 给每个障碍物添加移动
  for (let index = 0; index < obsList.length; index++) {
    let item = obsList[index]
    if (item.offsetLeft < -item.offsetWidth) {
      this.$refs.obstructionWrap.removeChild(item)
    } else {
      item.style.left = item.offsetLeft - this.speed * 3 + 'px'
    }
  }

玩法开发

玩法无非就是人物运动+障碍物运动+碰撞检测+躲避障碍物,人物运动和障碍物运动刚才都说过了,这里主要说的是碰撞检测和躲避障碍物,这个可以放到一起来说,因为只要没碰到那就是躲过去了。

这里的碰撞检测其实相当于抗疫的汤圆中的碰撞检测改版。其实准确的说,这里用的不是碰撞检测,是状态检测,因为这里的任务不能自由的移动,只有三种状态,跳跃、奔跑、下滑,所以我们只需要在合适的时候判断它处于什么状态就可以了,比如当上面的小恶魔过来的时候,判断人物是否处于下滑状态,如果不是,则判定为碰撞

首先我们需要找到和谁进行碰撞,因为同一时间障碍物可能有多个,我们给每一个障碍物添加碰撞检测,显然会浪费性能,所以我们需要找到距离离人物最近且没有完全经过人物的障碍物进行检测如下图所示

image.png

我们需要找到第一个自身没有完全经过人物的障碍物进行检测

已知人物宽度为120,人物处于屏幕水平中央

所以需要判断的障碍物距离左侧距离 > 屏幕宽度的一半 - 人物宽度的一半 - 障碍物自身的宽度

let nextItem = null

// 给每个障碍物添加移动
  for (let index = 0; index < obsList.length; index++) {
    // 由于只需要找到第一个符合条件的障碍物即可,所以这里需要进行判断
    if (!nextItem) {
      // 找到人物右侧最近的障碍物,进行碰撞检测
      // 需要进行检测的障碍物需满足条件:距离屏幕左侧距离>人物距离左侧距离+自身宽度
      // 人物宽度为120,人物距离左侧距离为屏幕的一半减去自身的一半
      if (item.offsetLeft > (this.screenWidth / 2 - 60 - item.offsetWidth)) {
        nextItem = item
      }
    }
  }

找到的检测目标,我们还需要知道什么时候进行检测,那就是当 障碍物距离屏幕左侧距离 < (屏幕宽度的一半 + 人物一半)的时候,这个时候障碍物刚好开始和障碍物进行重叠,此时在根据障碍物的上下位置(生成时保存下来的)和人物的状态进行判断,就可以判断出是否相撞了。

// 碰撞检测
  // 当距离最近的障碍物处于检测区时,进行碰撞检测
  if (nextItem.offsetLeft < (this.screenWidth / 2 + 60)) {
    if (nextItem.state === 'top') {
      if (this.userStatus !== 'slide') {
        this.$emit('gameOver')
        // 游戏结束
        // alert('游戏结束')
        // this.gameOver()
        return
      }
    } else {
      if (this.userStatus !== 'jump') {
        // 游戏结束
        this.$emit('gameOver')
        //  alert('游戏结束')
        //  this.gameOver()
        return
      }
    }
  }

碰撞检测是放在障碍物移动里进行的,完整代码如下。

// 整体障碍物移动
    obsMove () {
      // 获取所有障碍物
      let obsDoms = this.$refs.obstructionWrap.children
      let obsList = Array.from(obsDoms)

      let nextItem = null

      // 给每个障碍物添加移动
      for (let index = 0; index < obsList.length; index++) {
        let item = obsList[index]
        if (item.offsetLeft < -item.offsetWidth) {
          this.$refs.obstructionWrap.removeChild(item)
        } else {
          item.style.left = item.offsetLeft - this.speed * 3 + 'px'
        }

        // 由于只需要找到第一个符合条件的障碍物即可,所以这里需要进行判断
        if (!nextItem) {
          // 找到人物右侧最近的障碍物,进行碰撞检测
          // 需要进行检测的障碍物需满足条件:距离屏幕左侧距离>人物距离左侧距离+自身宽度
          // 人物宽度为120,人物距离左侧距离为屏幕的一半减去自身的一半
          if (item.offsetLeft > (this.screenWidth / 2 - 60 - item.offsetWidth)) {
            nextItem = item
          }
        }
      }

      // 碰撞检测
      // 当距离最近的障碍物处于检测区时,进行碰撞检测
      if (nextItem.offsetLeft < (this.screenWidth / 2 + 60)) {
        if (nextItem.state === 'top') {
          if (this.userStatus !== 'slide') {
            this.$emit('gameOver')
            // 游戏结束
            // alert('游戏结束')
            // this.gameOver()
            return
          }
        } else {
          if (this.userStatus !== 'jump') {
            // 游戏结束
            this.$emit('gameOver')
            //  alert('游戏结束')
            //  this.gameOver()
            return
          }
        }
      }

      // 找到最后一个障碍物,创建下一个障碍物
      let lastChild = obsList[obsList.length - 1]
      // console.log(lastChild.nextSpace);
      if (lastChild.offsetLeft < (this.screenWidth - lastChild.offsetWidth - lastChild.nextSpace)) {
        this.createObstruction()
      }
      this.obsMoveInterval = requestAnimationFrame(this.obsMove)
    },

小游戏介绍就到这了,希望大家能够喜欢,也给自己立个flag,如果能进前三,就给自己买个mac,如果能进前十,就给自己买个iPad,如果前十进不去,啥都不买了。