實戰——面向對象+原型實現貪喫蛇

面向對象+原型實現貪喫蛇

前面的那篇貪喫蛇(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、怎麼在之前的基礎上進行優化?

然後就不多解釋了,代碼中有註釋描述,直接上代碼

頁面代碼

<!-- 樣式部分 -->
<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,初始速度200</p>
  <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、什麼叫節流閥?
    解:用白話來說就是,多個人使用同一個東西,需要等別人用完這個東西之後你纔可以再使用,這就是節流閥的思想。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章