开发一个不走寻常路的贪吃蛇

流过昼夜 提交于 2020-02-27 04:59:46

学编程的小伙伴或多或少都写过贪吃蛇这个小游戏吧,核心的算法就是通过数组来维护蛇的移动和增长。具体实现方式:

地图:一个 M x N 的网格,每次移动的距离都是网格尺寸的 k 倍

移动:根据键盘按下的移动方向算出蛇头的位置,添加到数组顶部(unshift),同时移除数组的最后一个元素

进食:把食物的位置添加到数组顶部

碰撞:每次绘制前遍历数组,检测蛇头与身体每一块是否接触

原理很简单,实现起来也很简单,可以说是游戏领域的Hello World级的实例了。

那这篇博客的目的是把这个经典的算法再撸一遍?看过这个系列博客的筒子们都知道,这种网上随便一搜就是一堆的东西我是不屑于写的

说一下我本次贪吃蛇游戏的构想:

① 游戏能满帧运行,并且无视觉“卡顿”,而不是传统贪吃蛇的一格一格地往下走;

② 转动时尾巴逐渐收回,而不是突然消失;

③ 进食时身体逐渐增长,而不是突然变长;

④ 碰撞检测,没啥好说的;

⑤ 转动时蛇头也跟着旋转,回正时间与尾巴收回时长一致,而不是突然转过头;

⑥ 身体转折点圆弧过渡,碰撞检测时也检测圆弧;

⑦ 给身体,头、尾盖上贴图;

⑧ 将图片与骨骼绑定,做出蒙皮动画。

⑨ websocket实现多人联机

东西有些多,截止到目前(2020-02-24),我写这篇博客已经实现①②③④,其他的几个是未来将要完善的~~(又开始挖坑了。。

有人可能觉得第一点很简单,不就是把原来的大网格划分为1px * 1px 的小网格么,还是用经典的实现方式不就好了。这么想的人显然没有考虑过性能,先不说要逐像素来移动,光是要对所有像素点做碰撞检测就是极大的运算量(尽管可以算法的优化来过滤掉大部分,但是也不是一个好的解决方案,也不适合后面的贴图等扩展)。

老规矩,说下我的实现方式,首先还是定义一个对象数组,里面的每个对象的结构如下:

{
  direction: ???, // 每一截移动方向
  x: ???, y: ???, // 每一截的位置 
  width: ???, height: ??? // 每一截的尺寸 
}

然。。然后。。。然后我就把代码完整贴上来吧。。。。真不知道改怎么讲了,有疑问的可以评论区留言

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>贪吃蛇小游戏</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }

    html, body {
      width: 100%;
      height: 100%;
    }

    body {
      position: relative;
      background: lightgrey;
    }

    canvas {
      position: absolute;
      top: 50%;
      left: 50%;
      transform-origin: center;
      transform: translate3d(-50%, -50%, 0);
      border: 1px dashed grey;
    }
  </style>
</head>
<body>
<script>
  const CANVAS_WIDTH = 800
  const CANVAS_HEIGHT = 800

  const canvas = document.createElement('canvas')
  canvas.width = CANVAS_WIDTH
  canvas.height = CANVAS_HEIGHT
  document.body.appendChild(canvas)
  const ctx = canvas.getContext('2d')

  function checkCollision (box1, box2) {
    return !(box1.right <= box2.left || box1.left >= box2.right || box1.top >= box2.bottom || box1.bottom <= box2.top)
  }

  const DIR = {
    UP: 'up',
    LEFT: 'left',
    DOWN: 'down',
    RIGHT: 'right',
  }
  const KEY_CODE = {
    W: 87,
    A: 65,
    S: 83,
    D: 68,
  }

  const Snake = function () {
    this.score = 0
    this.crashed = false // 撞上自己
    this.headSize = { width: 20, height: 20 } // 头的宽/高
    this.partSize = { width: 20, height: 20 } // 每一节宽/高
    this.headPos = { x: CANVAS_WIDTH / 4, y: CANVAS_HEIGHT / 2 } // 头部的位置
    this.bodyLength = 200 // 身体长度
    this.disPlayBodyLength = 200 // 身体长度
    this.bodyParts = [
      { direction: DIR.RIGHT, x: this.headPos.x - 200, y: this.headPos.y, width: 200, height: this.partSize.height }
    ] // 身体组成部分的数组
    this.direction = DIR.RIGHT
    this.speed = 100 // 每秒移动距离
    this.foodPos = { x: 0, y: 0 } // 食物的位置
    this.foodSize = { width: 20, height: 20 } // 食物的尺寸
    this.eating = false
    this.headBox = {
      top: this.headPos.y,
      right: this.headPos.x + this.headSize.width,
      bottom: this.headPos.y + this.headSize.height,
      left: this.headPos.x,
    }
    this.foodBox = {
      top: this.foodPos.y,
      right: this.foodPos.x + this.foodSize.width,
      bottom: this.foodPos.y + this.foodSize.height,
      left: this.foodPos.x,
    }

    this.turn = function (keyCode) {
      if ((snake.direction === DIR.DOWN && keyCode === KEY_CODE.S) || (snake.direction === DIR.DOWN && keyCode === KEY_CODE.W)
        || (snake.direction === DIR.RIGHT && keyCode === KEY_CODE.D) || (snake.direction === DIR.RIGHT && keyCode === KEY_CODE.A)
        || (snake.direction === DIR.UP && keyCode === KEY_CODE.W) || (snake.direction === DIR.UP && keyCode === KEY_CODE.S)
        || (snake.direction === DIR.LEFT && keyCode === KEY_CODE.A) || (snake.direction === DIR.LEFT && keyCode === KEY_CODE.D)) {
        return false
      }
      const bodyPart = { x: this.headPos.x, y: this.headPos.y, width: this.partSize.width, height: this.partSize.height }
      switch (keyCode) {
        case KEY_CODE.W: // 上 W
          this.direction = DIR.UP
          this.headPos.y -= this.headSize.height
          bodyPart.direction = DIR.UP
          break
        case KEY_CODE.A: // 左 A
          this.direction = DIR.LEFT
          this.headPos.x -= this.headSize.width
          bodyPart.direction = DIR.LEFT
          break
        case KEY_CODE.S: // 下 S
          this.direction = DIR.DOWN
          this.headPos.y += this.headSize.height
          bodyPart.direction = DIR.DOWN
          break
        case KEY_CODE.D: // 右 D
          this.direction = DIR.RIGHT
          this.headPos.x += this.headSize.width
          bodyPart.direction = DIR.RIGHT
          break
      }
      // 增加转角长度然后减小
      this.disPlayBodyLength += this.partSize.width
      this.bodyParts.unshift(bodyPart)
    }

    this.move = function (delta) {
      if (this.disPlayBodyLength - this.bodyLength > 1) {
        this.disPlayBodyLength = this.bodyLength + (this.disPlayBodyLength - this.bodyLength) * 0.5
      }
      else {
        this.disPlayBodyLength = this.bodyLength
      }
      // 身体
      const bodyParts = []
      let nextHeadPos = Object.assign({}, this.headPos)
      const distance = delta / 1000 * this.speed
      switch (this.direction) {
        case DIR.UP: // 上
          nextHeadPos.y -= distance
          break
        case DIR.LEFT: // 左
          nextHeadPos.x -= distance
          break
        case DIR.RIGHT: // 下
          nextHeadPos.x += distance
          break
        case DIR.DOWN: // 右
          nextHeadPos.y += distance
          break
      }
      const nextHeadBox = {
        top: nextHeadPos.y,
        right: nextHeadPos.x + this.headSize.width,
        bottom: nextHeadPos.y + this.headSize.height,
        left: nextHeadPos.x,
      }
      let addedLength = 0
      if (this.bodyParts.length > 0) {
        let curPart
        let curPartBox
        let delta = 0
        // 临时长度(加上转角增加的部分)
        const bodyPartLength = this.disPlayBodyLength
        // const bodyPartLength = this.disPlayBodyLength + this.bodyParts.length * this.partSize.width
        let end = false
        for (let i = 0; !end && i < this.bodyParts.length; i++) {
          curPart = this.bodyParts[i]
          // 该部分方向向右
          if (curPart.direction === DIR.RIGHT) {
            if (i === 0) { curPart.width += distance }
            delta = addedLength + curPart.width - bodyPartLength
            // 若超过了总长度则截取
            if (delta >= 0) {
              curPart.x += delta
              curPart.width -= delta
              bodyParts.push(curPart)
              end = true
            }
            else {
              addedLength += curPart.width
              bodyParts.push(curPart)
            }
          }
          // 该部分方向向下
          else if (curPart.direction === DIR.DOWN) {
            if (i === 0) {
              curPart.height += distance
            }
            delta = addedLength + curPart.height - bodyPartLength
            // 若超过了总长度则截取
            if (delta >= 0) {
              curPart.y += delta
              curPart.height -= delta
              bodyParts.push(curPart)
              end = true
            }
            else {
              addedLength += curPart.height
              bodyParts.push(curPart)
            }
          }
          // 该部分方向向左
          else if (curPart.direction === DIR.LEFT) {
            if (i === 0) {
              curPart.x -= distance
              curPart.width += distance
            }
            delta = addedLength + curPart.width - bodyPartLength
            // 若超过了总长度则截取
            if (delta >= 0) {
              curPart.width -= delta
              bodyParts.push(curPart)
              end = true
            }
            else {
              addedLength += curPart.width
              bodyParts.push(curPart)
            }
          }
          // 该部分方向向上
          else if (curPart.direction === DIR.UP) {
            if (i === 0) {
              curPart.y -= distance
              curPart.height += distance
            }
            delta = addedLength + curPart.height - bodyPartLength
            // 若超过了总长度则截取
            if (delta >= 0) {
              curPart.height -= delta
              bodyParts.push(curPart)
              end = true
            }
            else {
              addedLength += curPart.height
              bodyParts.push(curPart)
            }
          }
          curPartBox = {
            top: curPart.y,
            right: curPart.x + curPart.width,
            bottom: curPart.y + curPart.height,
            left: curPart.x,
          }
          if (i !== 0 && !this.crashed && checkCollision(nextHeadBox, curPartBox)) {
            console.log(`和第${i + 1}截尾巴撞上了`)
            console.log('score:', this.score)
            this.crashed = true
          }
        }
      }
      this.headPos = nextHeadPos
      this.bodyParts = bodyParts
      this.headBox = nextHeadBox
    }

    // 和eat拆分开来是因为willEat必须每次循环调用,eat每帧调用,也许会穿过food
    this.willEat = function () {
      if (!this.eating && checkCollision(this.headBox, this.foodBox)) {
        this.eating = true
      }
    }

    this.eat = function () {
      if (this.eating) {
        this.bodyLength += this.foodSize.width
        this.disPlayBodyLength += this.foodSize.width
        this.createFood()
        this.speed *= 1.05
        this.eating = false
        this.score += 1
      }
    }

    this.createFood = function () {
      const foodPos = {
        x: Math.random() * (CANVAS_WIDTH - this.foodSize.width),
        y: Math.random() * (CANVAS_HEIGHT - this.foodSize.height),
      }
      const foodBox = {
        top: foodPos.y,
        right: foodPos.x + this.foodSize.width,
        bottom: foodPos.y + this.foodSize.height,
        left: foodPos.x,
      }
      // 头部是否碰撞
      if (checkCollision(this.headBox, foodBox)) {
        this.createFood()
      }
      else {
        // 循环遍历蛇身体,如果有碰撞则重新计算
        // for (let i = 0; i < this.bodyParts.length; i++) {
        //   ?? this.createFood()
        // }
        this.foodPos = foodPos
        this.foodBox = foodBox
      }
    }

    this.draw = function () {
      ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)
      // 蛇头
      ctx.fillStyle = 'rgba(0,10,10,0.5)'
      ctx.fillRect(this.headPos.x, this.headPos.y, this.headSize.width, this.headSize.height)
      // 蛇身
      ctx.fillStyle = 'rgba(0,80,80,0.5)'
      if (this.bodyParts.length > 0) {
        for (let i = 0; i < this.bodyParts.length; i++) {
          ctx.fillRect(this.bodyParts[i].x, this.bodyParts[i].y, this.bodyParts[i].width, this.bodyParts[i].height)
        }
      }

      // 食物
      ctx.fillStyle = 'rgba(0,2,127,1)'
      ctx.fillRect(this.foodPos.x, this.foodPos.y, this.foodSize.width, this.foodSize.height)
      // ctx.end()
    }

    this.update = function (delta) {
      this.eat()
      this.move(delta)
      this.draw()
    }
  }

  const snake = new Snake()
  snake.createFood()

  window.addEventListener('keydown', function (e) {
    const keyCode = e.keyCode
    if (keyCode === 32) {
      return stop ? play() : pause()
    }
    [KEY_CODE.W, KEY_CODE.A, KEY_CODE.S, KEY_CODE.D].indexOf(keyCode) !== -1 && snake.turn(keyCode)
  })

  let fps = 60  // 每秒帧数
  let lockFps = true // 锁定帧率
  let interval = 1000 / fps  // 连续帧之间间隔(理论)
  let stop = false  // 停止动画
  let timeLast = performance.now()  // 上一帧时间
  let delta = 0  // 连续帧之间间隔(实际)

  let distance = 0
  const tick = function (timestamp) {
    if (stop) return false
    snake.willEat()
    delta = timestamp - timeLast
    if (lockFps) {
      if (delta > interval) {
        snake.update(delta)
        timeLast = timestamp
      }
    }
    else {
      snake.update(delta)
      timeLast = timestamp
    }
    !snake.crashed && requestAnimationFrame(tick)
  }

  const pause = function () {
    stop = true

  }

  const play = function () {
    stop = false
    timeLast = performance.now()
    requestAnimationFrame(tick)
  }

  requestAnimationFrame(tick)

</script>

</body>
</html>

WASD控制方式,space暂停,当时为了调试用的,懒得去了2333

有几个缺陷:

  • 蛇头撞墙检测
  • 生成食物时禁止生成在身体上

就酱,后面补吧~23333

 完整代码戳这里

在线演示1在线演示2

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!