实战——面向对象+原型实现贪吃蛇

面向对象+原型实现贪吃蛇

前面的那篇贪吃蛇(https://blog.csdn.net/F_Felix/article/details/89676816)尽管也是用到了面向对象的,但是还有一些可以优化的地方,比如游戏本身同样是个对象,比如使用 JavaScript 中的原型
同样,我们先来分析一下这个贪吃蛇

思路分析

1、总共有几个对象? 这里有4个对象,分别是Snake、Food、Game、map,但是为了简单点就把map对象给省略了如果需要可以直接用之前那篇的
2、各个对象分别有哪些属性及方法呢?

  • Food对象:宽高、颜色、位置属性以及渲染方法(width、height、bgColor、x、y 和render)
  • Snake对象:宽高、方向、头颜色、身体颜色、身体属性以及渲染和移动方法(width、height、direction、headColor、bodyColor、body、render、move)
  • Game对象:地图、蛇、食物属性和初始化、初始化游戏、开始游戏、暂停游戏、重置游戏、结束游戏、操作蛇对象(map、snake、food、init、start、pause、control、reset)

3、怎么在之前的基础上进行优化?

  • 把方法定义在原型上,属性定义在构造函数中
  • 使用沙箱把代码包裹起来,避免全局污染
    (解释:https://blog.csdn.net/F_Felix/article/details/90111118)

然后就不多解释了,代码中有注释描述,直接上代码

页面代码


<style>
  * {
    margin: 0;
    padding: 0;
  }
  .map {
    width: 800px;
    height: 500px;
    background-color: #000;
    margin: 0 auto;
    position: relative;
  }
  p {
    text-align: center;
    line-height: 30px;
  }
  .btn {
    margin: 0 auto;
    text-align: center;
  }
style>

<body>
  <div class="btn">
    <button class="begin">开始游戏button>
    <button class="pause">暂停游戏button>
    <button class="reset">重启游戏button>
  div>
  <p>
    <strong>游戏说明:strong>Enter--> 开始游戏 Space--> 暂停游戏 Esc--->
    重置游戏 ←↑→↓ 控制方向
  p>
  <p>蛇每吃10个点速度增加,极限速度50,初始速度200p>
  <div class="map">div>
  <script src="food.js">script>
  <script src="snake.js">script>
  <script src="game.js">script>
  <script>
    var map = document.querySelector('.map')
    var btn1 = document.querySelector('.begin')
    var btn2 = document.querySelector('.pause')
    var btn3 = document.querySelector('.reset')
    var g = new Game({
      map: map
    })
    g.init()

    btn1.onclick = function() {
      g.start()
    }
    btn2.onclick = function() {
      g.pause()
    }
    btn3.onclick = function() {
      g.reset()
    }
    // 解决js中点击了一次按钮,再按回车会触发之前点击按钮的事件
    document.onkeydown = function(e) {
      switch (e.keyCode) {
        case 13:
          btn1.focus()
          break
        case 32:
          btn2.focus()
          break
        case 27:
          btn3.focus()
          break
      }
    }
  script>
body>

Food 对象

// 优化4---- 把代码放在沙箱里
;(function(window) {
  /**
   * 食物构造函数
   * @param {object} options 对象参数
   * 食物属性:宽度、高度、颜色、位置
   * 食物方法:渲染
   */
  function Food(options) {
    options = options || {}
    this.width = options.width || 20
    this.height = options.height || 20
    this.bgColor = options.bgColor || 'cyan'
    this.x = 0
    this.y = 0
  }

  Food.prototype.render = function(target) {
    // 优化1,每次渲染之前需要把之前的点移除了,主要是用于蛇吃到点后
    this.node && target.removeChild(this.node)

    // 创建食物圆点,并且把圆点添加到map中去
    var div = document.createElement('div')
    this.node = div
    target.appendChild(div)
    div.style.width = this.width + 'px'
    div.style.height = this.height + 'px'
    div.style.backgroundColor = this.bgColor
    div.style.position = 'absolute'
    this.x = ((Math.random() * target.offsetWidth) / this.width) | 0
    this.y = ((Math.random() * target.offsetHeight) / this.height) | 0
    div.style.left = this.x * this.width + 'px'
    div.style.top = this.y * this.height + 'px'
    div.style.borderRadius = '50%'
  }
  window.Food = Food
})(window)

Snake 对象

;(function(window) {
  /**
   * 蛇构造函数
   * @param {*} options 蛇的一些属性(对象)
   * 蛇属性:每一节的宽度 高度,颜色,位置,蛇头的方向
   * 蛇方法:渲染、移动
   */
  function Snake(options) {
    options = options || {}
    this.width = options.width || 20
    this.height = options.height || 20
    this.direction = options.direction || 'right'
    this.headColor = options.headColor || 'skyblue'
    this.bodyColor = options.bodyColor || 'yellowgreen'
    this.body = [{ x: 2, y: 0 }, { x: 1, y: 0 }, { x: 0, y: 0 }]
  }
  // 把蛇渲染到map中
  Snake.prototype.render = function(target) {
    // 优化2 渲染蛇之前把之前的蛇移除掉
    var spans = document.querySelectorAll('span')
    for (var i = 0; i < spans.length; i++) {
      target.removeChild(spans[i])
    }
    for (var i = 0; i < this.body.length; i++) {
      var span = document.createElement('span')
      target.appendChild(span)
      span.style.width = this.width + 'px'
      span.style.height = this.height + 'px'
      span.style.backgroundColor = i === 0 ? this.headColor : this.bodyColor
      span.style.position = 'absolute'
      span.style.left = this.body[i].x * this.width + 'px'
      span.style.top = this.body[i].y * this.height + 'px'
      span.style.borderRadius = '50%'
    }
  }
  // 蛇的移动
  Snake.prototype.move = function(target, food) {
    /* 
        移动逻辑:
            复制一个蛇头节点,控制这个节点进行移动
            如果没有吃到点那么就删除蛇尾的点
            如果吃到了点则不删除结尾的点
        其实逻辑与让后一个点移动到前一个点的逻辑是一致的
    */
    var head = {
      x: this.body[0].x,
      y: this.body[0].y
    }
    switch (this.direction) {
      case 'right':
        head.x++
        break
      case 'left':
        head.x--
        break
      case 'up':
        head.y--
        break
      case 'down':
        head.y++
        break
    }
    this.body.unshift(head)
    // 当蛇头的位置和点的位置重叠后,就是吃到了点
    if (head.x == food.x && head.y == food.y) {
      // 重新渲染点,因此对food渲染进行优化 ---> 优化1
      food.render(target)
    } else {
      this.body.pop()
    }
    // 优化4 当判断蛇撞边界时,不会继续执行渲染导致蛇头超出边界
    if (
      head.x < 0 ||
      head.y < 0 ||
      head.x >= target.offsetWidth / this.width ||
      head.y >= target.offsetHeight / this.height
    ) {
      return
    }
    // 优化2 重新渲染蛇的时候,同样需要把之前的蛇给移除了,然后在渲染
    this.render(target)
  }
  window.Snake = Snake
})(window)

Game 对象

// 优化4 -- 把代码放在沙箱中
;(function(window) {
  /**
   * 游戏对象
   * @param {*} options 游戏对象参数
   * 属性包括:地图、蛇、食物、定时器时间
   * 方法:初始化游戏、开始游戏、暂停游戏、重置游戏、结束游戏、操作蛇对象(添加键盘事件)
   */
  function Game(options) {
    options = options || {}
    this.map = options.map
    this.snake = options.snake || new Snake()
    this.food = options.food || new Food()
    this.duration = 200 // 初始速度
    this.limit = 50 // 速度最小是50ms
    this.level = 10 // 每吃到十个点就加速
  }

  // 优化3  给游戏增加节流阀
  var dirFlag = true // 方向节流阀---> 避免因为方向按得过快导致的,可以反向运动
  var gameFlag = true // 游戏节流阀 ---> 避免因为多次点击开始游戏导致的定时器失效

  var tId // 定时器id
  var these // 用于存储在start中调用定时器时使用的this,必须是全局变量,不然在定时器中无法识别
  var prevLength = 3 // 初始时蛇的长度为3

  // 初始化游戏
  Game.prototype.init = function() {
    this.snake.render(this.map)
    this.food.render(this.map)
    // 监听事件可以放这里也可以放在start里
    this.control()
  }

  // 游戏开始
  Game.prototype.start = function() {
    if (gameFlag) {
      gameFlag = false
      // 因为这里的this和定时器里的this不是同一个指向,因此先保存一下
      // var that = this;
      // this.timeId = setInterval(function () {
      //     // 注意:这里的this和外面的this不是同一个,这个指向的是window对象
      //     that.snake.move(that.map, that.food);
      //     // 游戏结束
      //     that.gameOver();

      //     // 暴力设置经过多少时间后节流阀重启
      //     setTimeout(() => {
      //         dirFlag = true;
      //     }, 100);
      // }, this.duration);
      these = this
      // tId = setInterval('execute(these)', this.duration)
      tId = setInterval(() => {
        execute(these)
      }, this.duration)
    }
  }

  // 递归调用,让速度越来越快
  function execute(these) {
    // 注意:这里的this和外面的this不是同一个,这个指向的是window对象
    these.snake.move(these.map, these.food)
    // 游戏结束
    these.gameOver()

    clearInterval(tId)

    if (
      these.snake.body.length - prevLength >= these.level &&
      these.duration >= these.limit
    ) {
      prevLength = these.snake.body.length
      these.duration -= 10
      console.log(these.duration)
    }
    // var that = tt;
    // 如果游戏节流阀重启了,表示前一次游戏结束了,那么就不再开启定时器
    if (!gameFlag) {
      tId = setInterval(() => {
        execute(these)
      }, these.duration)
    }

    // 暴力设置经过多少时间后节流阀重启
    setTimeout(() => {
      dirFlag = true
    }, 100)
  }
  // 游戏结束
  Game.prototype.gameOver = function() {
    var head = this.snake.body[0]
    // 1、撞到了边界--->死
    if (
      head.x < 0 ||
      head.y < 0 ||
      head.x >= this.map.offsetWidth / this.snake.width ||
      head.y >= this.map.offsetHeight / this.snake.height
    ) {
      alert('Game Over!')
      this.pause()
      gameFlag = true
    }
    // 2、撞到了自己--->死
    for (var i = 4; i < this.snake.body.length; i++) {
      if (head.x == this.snake.body[i].x && head.y == this.snake.body[i].y) {
        alert('Game Over!')
        this.pause()
        gameFlag = true
      }
    }
  }

  // 暂停游戏
  Game.prototype.pause = function() {
    // clearInterval(this.timeId);
    clearInterval(tId)
    gameFlag = true
    dirFlag = true
  }

  // 操纵蛇移动以及游戏的开始、暂停、重置
  Game.prototype.control = function() {
    var that = this
    document.addEventListener('keydown', function(e) {
      if (dirFlag) {
        dirFlag = false
        switch (e.keyCode) {
          case 37:
            if (that.snake.direction != 'right') {
              that.snake.direction = 'left'
            }
            break
          case 38:
            if (that.snake.direction != 'down') {
              that.snake.direction = 'up'
            }
            break
          case 39:
            if (that.snake.direction != 'left') {
              that.snake.direction = 'right'
            }
            break
          case 40:
            if (that.snake.direction != 'up') {
              that.snake.direction = 'down'
            }
            break
          case 32:
            that.pause()
            break
          case 13:
            that.start()
            break
          case 27:
            that.reset()
            break
        }
      }
    })
  }

  // 重置游戏
  Game.prototype.reset = function() {
    this.snake.body = [{ x: 2, y: 0 }, { x: 1, y: 0 }, { x: 0, y: 0 }]
    this.snake.direction = 'right'
    this.init()
    // clearInterval(this.timeId);
    clearInterval(tId)
    gameFlag = true
    dirFlag = true
  }

  window.Game = Game
})(window)

针对游戏中的方法进行简单解释一下

1、为什么需要these = this?(start方法中)
    解: 因为在定时器中,this是指向window的,但是我们的this需要指向的是Game对象,因此我们把这个this赋值给了these,这样我们就可以在后面通过使用these相当于使用Game了。(其实这里不用these = this也可以,因为使用了指针函数)
    如果没有使用指针函数,如下,还有另一种解决方法通过使用'上下文调用模式'中的bind方法,让定时器里的this指向了Game对象的this
    tId = setInterval(function(){
        execute(this)
    }.bind(this),this.duration)
2、为什么这里要另外调用一个execute方法?
    解:为了实现能够在吃掉一定数量的点之后可以让蛇的速度越来越快,但是如果在function中直接修改this.duration是不起作用的,因此为了实现效果,我们就需要通过递归调用的方式,每次先清除之前的定时器,然后在创建新的定时器的方式来实现速度越来越快
3、两个节流阀的作用?
    解:大家可以试一下,就是如果把两个节流阀去掉,方向节流阀如果去掉就会因为我们有时候按方向键过快,导致蛇可以直接反向运动然后咬身体over,举例说:一开始蛇往右移动,但是你在非常短的时间里,先按了上(或者下)然后在按了左,这样蛇就会瞬间由朝右运动变为朝左,然后要自己了。至于游戏节流阀,如果没有这个节流阀,就会导致我可以一直按开始按妞,然后因为定时器的存在,会导致蛇越来越快,并且最后蛇停不下来了,因此才会加上这个节流阀。
4、什么叫节流阀?
    解:用白话来说就是,多个人使用同一个东西,需要等别人用完这个东西之后你才可以再使用,这就是节流阀的思想。

你可能感兴趣的:(前端开发)