vue2 贪吃蛇的实现思路

我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛

前言

  之前用原生的JavaScrip写过一次贪吃蛇,逻辑稍显复杂,后面用JQuery做过一个优化,但是整体的效果不是很好,这里我们用vue2来粗略地实现。

思路

  要让一条蛇在画板上跑,因此首先我们要新建一个画板元素div,先暂时固定宽高,然后给定一个背景色,样式后面再来优化。

截取图片_20220419145139.png

  然后我们再来放一条蛇,新增一个list的数据,然后初始化它的坐标为(0, 0)、(0, 1)、(0, 2)、(0, 3)、(0, 4),其中x表示每一个元素位于多少行,y表示位于多少列。

this.list = new Array(length).fill(0).map((el, index) => ({ x: 0, y: index }))
复制代码

  然后我们要v-if渲染到画板上,同时配上位置函数,将每个元素都位置都放好。

getPosotion({ x, y }) {
  return {
    top: `${x * 20}px`,
    left: `${y * 20}px`,
  }
},
复制代码

image.png

  现在我们来让这条蛇元素动起来,很简单,列表中的数据,后一个的坐标来覆盖前一个的坐标即可,而最后一个元素,也就是蛇头,我们固定让它的y自增1就可以了。

const list = [...this.list]
const len = list.length
const head = list[len - 1]

list.forEach((item, index) => {
  if (index < len - 1) {
    const next = list[index + 1]

    ;[item.x, item.y] = [next.x, next.y]
  }
})

head.y += 1
复制代码

  现在它就动起来了。

image.png

  现在来想一个问题呢,如果定时器一直跑,蛇就跑出画板了,同时我们还不能控制方向,因此我们需要监听键盘的按下事件,同时,我们需要一个current值来保存当前的方向。

addEventListener() {
  document.addEventListener('keydown', ({ keyCode }) => {
     this.current = keyCode
  })
},


复制代码

  然后在移动的函数中,根据当前的方向值,来让蛇上下左右移动。

const keyCodes = {
  LEFT: 37,
  TOP: 38,
  RIGHT: 39,
  BOTTOM: 40,
}

switch (this.current) {
  case keyCodes.RIGHT:
    head.y += 1
    break
  case keyCodes.LEFT:
    head.y -= 1
    break
  case keyCodes.BOTTOM:
    head.x += 1
    break
  case keyCodes.TOP:
    head.x -= 1
    break
}

this.list = list
复制代码

  现在,我们的大蛇可以上下左右移动了。

image.png

  然后再来考虑碰撞问题,无非就是四个边界,还有就是自身也可能发生碰撞,这里直接上代码。边界碰撞只需要判断头部元素的坐标值和边界之间的关系,而自身碰撞,需要头部坐标与除了头部以外的剩余蛇身体部分的坐标做比较,如果有一个相等,some再好不过,那么就是碰撞了自身,游戏将结束。

isImpact() {
  const len = this.list.length
  const { x: headX, y: headY } = this.list[len - 1]
  const { cols, rows } = this.getRowsCols()

  if (this.list.slice(0, len - 1).some(({ x, y }) => x === headX && y === headY)) {
    return true
  }

  if (headY >= cols) {
    return true
  }

  if (headX >= rows) {
    return true
  }

  if (headX < 0) {
    return true
  }

  if (headY < 0) {
    return true
  }

  return false
},
复制代码

  这里就是自己撞自己的情况。

image.png

  以上情况都解决之后呢,我们再来考虑生成苹果的情况,蛇嘛,让吃此苹果就行了。逻辑上也很简单,根据画布的情况,随机生成一个苹果,然后我们要判断每一个方向时,只要苹果的坐标和蛇头的坐标满足条件与否。举个栗子,假设蛇往右移动,那么就是currentRIGHTkeycode时,且苹果的x和蛇头的x相同,而蛇头的y + 1是等于苹果的y的话,表明蛇可以吃苹果,然后我们往listpush一个空对象就可以了。为什么是空对象呢,因为下一次移动时,空对象将被上一个对象覆盖。

canEat() {
  const len = this.list.length
  const { x: headX, y: headY } = this.list[len - 1]
  const { x, y } = this.apple

  if (this.current === keyCodes.RIGHT) {
    if (headY + 1 === y && headX === x) {
      return true
    }
  }

  if (this.current === keyCodes.LEFT) {
    if (headY - 1 === y && headX === x) {
      return true
    }
  }

  if (this.current === keyCodes.TOP) {
    if (headX - 1 === x && headY === y) {
      return true
    }
  }

  if (this.current === keyCodes.BOTTOM) {
    if (headX + 1 === x && headY === y) {
      return true
    }
  }

  return false
},
复制代码

  一个简单的贪吃蛇就完成了,你可以添加一些得分,或者暂停按钮,或者其它的样式,这里是完整的代码。

<template>
  <div id="app">
    <div v-for="(item, index) in list" :key="index" class="item" :style="getPosotion(item)"></div>
    <button @click="pause">暂停</button>
    <div v-if="show" class="dialog">
      <p>你已经GG了!!!</p>
      <p class="start" @click="handleRePlay">重新开始</p>
    </div>
    <div v-if="showApple" class="apple" :style="getPosotion(apple)"></div>
  </div>
</template>

<script>
const keyCodes = {
  LEFT: 37,
  TOP: 38,
  RIGHT: 39,
  BOTTOM: 40,
}
const length = 5
const speed = 300

export default {
  name: 'App',
  data() {
    return {
      list: [],
      current: null,
      timer: null,
      show: false,
      showApple: false,
      apple: {
        x: -1,
        y: -1,
      },
    }
  },
  mounted() {
    this.init()
    this.addEventListener()
  },
  beforeDestroy() {
    clearInterval(this.timer)
  },
  methods: {
    handleRePlay() {
      this.show = false
      this.init()
    },

    init() {
      this.current = keyCodes.RIGHT
      this.start()
    },

    start() {
      this.list = new Array(length).fill(0).map((el, index) => ({ x: 0, y: index }))

      clearInterval(this.timer)
      this.timer = setInterval(() => {
        this.move()
      }, speed)
    },

    move() {
      const list = [...this.list]
      const len = list.length
      const head = list[len - 1]

      list.forEach((item, index) => {
        if (index < len - 1) {
          const next = list[index + 1]

          ;[item.x, item.y] = [next.x, next.y]
        }
      })

      switch (this.current) {
        case keyCodes.RIGHT:
          head.y += 1
          break
        case keyCodes.LEFT:
          head.y -= 1
          break
        case keyCodes.BOTTOM:
          head.x += 1
          break
        case keyCodes.TOP:
          head.x -= 1
          break
      }

      this.list = list

      if (!this.showApple) {
        this.showApple = true
        this.createApple()
      }

      if (this.canEat()) {
        this.list.unshift({})
        this.showApple = false
      }

      if (this.isImpact()) {
        this.pause()
        this.show = true
        this.showApple = false
      }
    },

    canEat() {
      const len = this.list.length
      const { x: headX, y: headY } = this.list[len - 1]
      const { x, y } = this.apple

      if (this.current === keyCodes.RIGHT) {
        if (headY + 1 === y && headX === x) {
          return true
        }
      }

      if (this.current === keyCodes.LEFT) {
        if (headY - 1 === y && headX === x) {
          return true
        }
      }

      if (this.current === keyCodes.TOP) {
        if (headX - 1 === x && headY === y) {
          return true
        }
      }

      if (this.current === keyCodes.BOTTOM) {
        if (headX + 1 === x && headY === y) {
          return true
        }
      }

      return false
    },

    createApple() {
      const { cols, rows } = this.getRowsCols()
      const x = getRandomIntInclusive(0, rows - 1)
      const y = getRandomIntInclusive(0, cols - 1)

      this.apple = {
        x,
        y,
      }

      function getRandomIntInclusive(min, max) {
        min = Math.ceil(min)
        max = Math.floor(max)
        return Math.floor(Math.random() * (max - min + 1)) + min
      }
    },

    getRowsCols() {
      const { width: appWidth, height: appHeight } = document.querySelector('#app').getBoundingClientRect()
      const item = document.querySelector('.item')
      const { width, height } = item.getBoundingClientRect()

      return {
        cols: ~~(appHeight / height),
        rows: ~~(appWidth / width),
      }
    },

    isImpact() {
      const len = this.list.length
      const { x: headX, y: headY } = this.list[len - 1]
      const { cols, rows } = this.getRowsCols()

      if (this.list.slice(0, len - 1).some(({ x, y }) => x === headX && y === headY)) {
        return true
      }

      if (headY >= cols) {
        return true
      }

      if (headX >= rows) {
        return true
      }

      if (headX < 0) {
        return true
      }

      if (headY < 0) {
        return true
      }

      return false
    },

    pause() {
      clearInterval(this.timer)
    },

    addEventListener() {
      document.addEventListener('keydown', ({ keyCode }) => {
        if (this.canBack(keyCode)) {
          this.current = keyCode
        }
      })
    },

    canBack(keyCode) {
      const len = this.list.length
      const prev = this.list[len - 2]
      const head = this.list[len - 1]

      if (prev.y === head.y) {
        // 向下移动时,不能往上移动
        if (this.current === keyCodes.BOTTOM && keyCode === keyCodes.TOP) {
          return false
        }

        // 向上移动时,不能往下移动
        if (this.current === keyCodes.TOP && keyCode === keyCodes.BOTTOM) {
          return false
        }
      }

      if (prev.x === head.x) {
        // 向左移动时,不能往右移动
        if (this.current === keyCodes.LEFT && keyCode === keyCodes.RIGHT) {
          return false
        }

        // 向右移动时,不能往左移动
        if (this.current === keyCodes.RIGHT && keyCode === keyCodes.LEFT) {
          return false
        }
      }

      return true
    },

    getPosotion({ x, y }) {
      return {
        top: `${x * 20}px`,
        left: `${y * 20}px`,
      }
    },
  },
}
</script>

<style lang="scss">
#app {
  margin-top: 60px;
  width: 500px;
  height: 500px;
  background: #00cc99;
  border-radius: 5px;
  margin: 0 auto;
  position: relative;
  // overflow: hidden;
}

.item,
.apple {
  position: absolute;
  width: 20px;
  height: 20px;
  background-color: red;
  border-right: 1px solid #fff;
  box-sizing: border-box;
  border-radius: 5px;
}

.apple {
  background-color: pink;
}

button {
  transform: translateX(-100%);
}

.dialog {
  background-color: rgba(0, 0, 0, 0.5);
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 20px;

  .start {
    background-color: green;
    font-size: 16px;
    padding: 5px;
    cursor: pointer;
    border-radius: 5px;
  }
}
</style>
复制代码

  最后的效果。

image.png

猜你喜欢

转载自juejin.im/post/7088209909157724196
今日推荐