5
头图

写在前面

SegmentFault 思否的吉祥物是一只独一无二、特立独行、热爱自由的(>^ω^<)独角猫,也是社区的首席摸鱼官。于是本次作品是以思否猫为原型搭配原生js完成了一个类似跑酷的小游戏

游戏链接

https://simplerobort.github.i...
image.png

技术栈构成

(javascript做游戏可用的引擎:1、Babylon.js;2、Three.js;3、Turbulenz;4、Famo.us;5、PlayCanvas.js;6、Goo Engine;7、CooperLicht;8、Voxel等。)
由上可见js能够使用的游戏引擎还挺多,最近为了进军元宇宙在学threeJs,为了更好锻炼对于游戏的理解,因此没有使用任何游戏引擎(有轮子我偏不用,我造!)
采用的技术栈为: vue+vuex+vueRouter+原生js(人物的碰撞,跳跃,自由落体等)

实现过程与思考

设计思路

效果图如下(障碍物很潦草哈哈)
image.png

  • 将人物作为一个模块,将移动、死亡、跳跃相关代码逻辑放置在人物模块,主要是觉得这样更加合理,是因为有了人物才会有这个功能,所以这些功能应该和人物强绑定
  • 将障碍物,触碰会死亡的障碍物作为模块复用
  • store管理公共状态:当前已过关卡、障碍物信息、人物是否正在移动相关信息
  • router管理页面:欢迎页面、每一个关卡、死亡页面、胜利页面
  • 游戏核心模块:人物移动跳跃、物理碰撞

人物实现

人物样式

人物的静止、跳跃、死亡使用gif,通过双向绑定src,根据逻辑替换就完成了任务状态的变化

<template>
 <div class="human" :style="{left:Hleft,bottom:Hbottom}" ref="human">
   <img :src="humanPic" alt="">
 </div>
</template>

左右移动

从人物样式的代码截图可以看到,人物的移动其实只是将人物的left与bottom双向绑定到组件状态中,在之后便可以实现通过快速修改left与bottom来完成人物的移动

左右移动的逻辑:
在人物组件初始化时会调用moveOpen并且moveOpen只做了三件事

  • 监听键盘按下
  • 监听键盘弹起
  • 调用moveClock
moveOpen: function () {
      document.onkeydown = () => {...
      }
      document.onkeyup = () => {...
      }
      this.moveClock()
    },

下面详细聊聊这三件事内都做了什么

1.监听键盘按下

假设我们现在按住了D键移动,其实做了这两件事

  • 如果已经是死亡状态就停止逻辑(因为死亡有动画,防止死亡动画内还能移动)
  • goRight函数内将人物的向右移动状态改为true
 document.onkeydown = () => {
        if (this.isDead) return
        const key = window.event.keyCode
        switch (key) {
          case 65:
            // A
            this.goLeft(0)
            break
          case 68:
            // D
            this.goRight(0)
            break
          case 87:
          case 32:
            // W
            this.jump(8)
            break
        }
      }
    goLeft: function (type) {
      if (type === 0) {
        this.changeStatus('isLeft', true) // changeStatus就是一个修改store的封装
      } else if (type === 1) {
        this.changeStatus('isLeft', false)
      }
    },
    goRight: function (type) {
      if (type === 0) {
        this.changeStatus('isRight', true)
      } else if (type === 1) {
        this.changeStatus('isRight', false)
      }
    },

2.监听键盘弹起

弹起只做了一件事:将人物的对应的移动状态改为false

      document.onkeyup = () => {
        const key = window.event.keyCode
        switch (key) {
          case 65:
            // A
            this.goLeft(1)
            break
          case 68:
            // D
            this.goRight(1)
            break
        }
      }

3.调用moveClock

moveclock其实就是开启了一个定时器,每次执行4件事

  • 副作用拦截:在未移动状态或者死亡状态不执行,走return
  • 游戏界面限制:当小人在左边或者最右边不允许继续走出
  • 障碍物:判断是否碰到障碍物来决定是否死亡或者胜利或者停止移动
  • 下坠:判断是否符合下坠条件
  • 移动:最后执行移动
    moveClock: function () {
      setInterval(() => {
        const { human } = this.$refs
        const { cantgo, freeFall } = this.$store.getters
        const { stayIndex, humanInfo } = this.$store.state
        const Hleft = parseFloat(this.Hleft)
        const Hbottom = parseFloat(this.Hbottom)

        // 因为计时器一直在走,不在左右移动状态或者在死亡状态时不执行逻辑
        if (this.isDead || (!humanInfo.isLeft && !humanInfo.isRight)) return
        // 当小人在最左边或者最右边时不允许在移动
        const condition = humanInfo.isLeft
          ? human.offsetLeft <= 0
          : humanInfo.isRight && (human.offsetLeft + 15) >= 1280
        if (condition) {
          this.Hleft = humanInfo.isLeft ? '0px' : '1265px'
          return
        }
        // 判断是否碰到了障碍物来决定是走路还是死亡还是胜利还是停止
        const left = humanInfo.isLeft ? Hleft - 5 : Hleft + 15
        if (cantgo(left, Hbottom, 'stopArea')) return
        if (cantgo(left, Hbottom, 'deadArea')) return this.gotDead()
        if (cantgo(left, Hbottom, 'winArea')) return this.win()
        // stayindex不是-1就代表当前踩的是障碍物而不是地面
        // freefall判断有没有走出障碍物,走出就下坠
        if (stayIndex !== -1 && freeFall(Hleft)) this.jump(0)

        // 最后执行移动
        const forWard = humanInfo.isLeft ? -2.5 : 2.5
        this.Hleft = Hleft + forWard + 'px'
      }, 10)
    }

人物跳跃

人物跳跃为了仿真逐渐变慢直到下降落地的过程采用了递归递减
人物跳跃主要做了三件事

  • 拦截副作用:当已经在跳跃状态或者死亡状态不允许再次跳跃
  • 修改跳跃状态:修改跳跃状态为真,将人物gif换成跳跃的gif
  • 跳跃计算:执行一个递归函数jp

jp函数内部主要做了3件事

  • 判断是否碰到禁止移动障碍物,触碰后如果是上升状态就将速度修正为0并且开始下降行为,如果是下降状态就停止跳跃行为
  • 判断是否碰到死亡障碍物或者是胜利障碍物来决定是否死亡与进入胜利画面
  • 判断这次位移后是否碰到地面,碰到就停止跳跃,若没有碰到就减速度并且递归jp函数直到碰到障碍物或者地面
    jump: function (speed) {
      // 如果死亡或者正在跳跃拒绝发起跳跃行为
      if (this.isDead || this.$store.state.humanInfo.isJump) return
      // 将跳跃状态改为真
      this.changeStatus('isJump', true)
      this.humanPic = require('@/assets/human/jump1.0.1.gif')
      const jp = () => {
        const { cantJump } = this.$store.getters
        const jpheight = parseFloat(this.Hbottom)
        const Hleft = parseFloat(this.Hleft)
        // 判断上升或者下落时是否碰到障碍物触发特定行为
        if (cantJump(speed > 0, jpheight, Hleft, 'stopArea')) {
          // 当前处于下降就停止
          if (speed <= 0) return this.jumpStop()
          // 当前处于上升就直接将速度改为0变成下降
          speed = 0
        }
        if (cantJump(speed > 0, jpheight, Hleft, 'deadArea')) return this.gotDead()
        if (cantJump(speed > 0, jpheight, Hleft, 'winArea')) return this.win()
        // 判断是否碰地来选择继续上升下降还是停止跳跃
        const isTouchLand = jpheight + speed <= 187.61
        this.Hbottom = isTouchLand ? '187.61px' : jpheight + speed + 'px'
        if (isTouchLand) {
          this.$store.commit('changestayIndex', -1)
          return this.jumpStop()
        }
        // 没有碰到任何意外情况,减速度继续递归
        speed -= 0.2
        setTimeout(jp, 10)
      }
      jp()
    },

障碍物实现

障碍物初始化
目前共有三种类型的障碍物:死亡障碍物、停止障碍物、胜利障碍物
并且是以无状态组件的方式完成,目的是为了只需要关注障碍物的位置而不是我要用一次就要写一次逻辑
可以看到障碍物生成时只是把自己的宽高定位信息传给了store,store会把信息保存在数组里

<template>
  <div id="rock" class="onTheLand" :style="{left:this.rleft,width:this.rwidth,height:this.rheight,bottom:this.rbottom}"></div>
</template>

<script>
export default {
  name: 'rock',
  props: ['rleft', 'rwidth', 'rheight', 'rbottom'],
  created () {
    this.init()
  },
  methods: {
    init: function () {
      let obj = {
        rl: parseFloat(this.rleft.replace('px', '')),
        rw: parseFloat(this.rwidth.replace('px', '')),
        rh: parseFloat(this.rheight.replace('px', '')),
        rb: parseFloat(this.rbottom.replace('px', ''))
      }
      this.$store.commit('addStopArea', obj)
    }
  }
}
</script>

<style scoped>
  #rock {
    border: 2px solid black;
    /*background-color: black;*/
  }
</style>

障碍物碰撞原理

人物在每次移动时会调用store提供的api来判断是否碰到某一类型的障碍物,所以只需要实现障碍物碰撞的计算判断即可
人物移动时根据障碍物暴露的三个api来完成碰撞计算:cantgo、cantJump、freeFall

cantgo是人物左右移动时判断是否碰到障碍物的api,传入三个参数:人物的left、人物的bottom、需要判断的障碍物类型
进行障碍物判断的传的left其实是人物假设移动以后的left,判断的方式是循环判断移动后的left是否在障碍物内部,如果是的就代表碰到了

    cantgo: function (state) {
      return function (left, feet, area) {
        if (!state.humanInfo.isRight && !state.humanInfo.isLeft) return false
        return state[area].some(
          item => (
            left >= item.rl &&
            left < (item.rl + item.rw) &&
            feet >= item.rb &&
            feet < (item.rb + item.rh)
          )
        )
      }
    },

cantJump是人物跳跃时是否碰到障碍物的api,与cantgo同理,只是新增一个是否在上升阶段,因为降落阶段如果碰到了障碍物停止,会保存障碍的index到stayindex,这个值在freeFall会用到

    cantJump: function (state) {
      return function (isUp, feet, left, area) {
        if (isUp) {
          return state[area].some(
            item => (
              (feet + 60) >= item.rb &&
              (feet + 60) < (item.rb + item.rh) &&
              (left + 15) >= item.rl &&
              left <= (item.rl + item.rw)
            )
          )
        } else {
          return state[area].some(
            (item, index) => {
              if (feet >= item.rb &&
                  feet <= (item.rb + item.rh + 6) &&
                  (left + 15) >= item.rl &&
                  left <= (item.rl + item.rw)
              ) {
                state.stayIndex = index
                return true
              }
            }
          )
        }
      }
    },

freeFall是在障碍物上移动时判断是否走出障碍物的api
通过stayIndex判断在哪个障碍物上,在判断left是否小于障碍物左侧或者大于右侧

    freeFall: function (state) {
      return function (left) {
        if (state.isJump) return false
        const isRightDown = state.humanInfo.isRight &&
            left > (state.stopArea[state.stayIndex].rl + state.stopArea[state.stayIndex].rw)
        const isLeftDown = state.humanInfo.isLeft &&
            (left + 15) <= state.stopArea[state.stayIndex].rl
        return isLeftDown || isRightDown
      }
    }

结束

前端一昧的开发必定枯燥,要能够从枯燥的生活中找到你工作的乐趣

本文参与了1024程序员节,欢迎正在阅读的你也加入。

Yangπ
23 声望2 粉丝