微信小遊戲 demo 飛機大戰 代碼分析 (一)(game.js, main.js)

微信小遊戲 demo 飛機大戰 代碼分析(一)(main.js)

微信小遊戲 demo 飛機大戰 代碼分析(二)(databus.js)html

微信小遊戲 demo 飛機大戰 代碼分析(三)(spirit.js, animation.js)canvas

微信小遊戲 demo 飛機大戰 代碼分析(四)(enemy.js, bullet.js, index.js)數組

本博客將使用逐行代碼分析的方式講解該demo,本文適用於對其餘高級語言熟悉,對js還未深刻了解的同窗,博主會盡量將全部遇到的不明白的部分標註清楚,如有不正確或不清楚的地方,歡迎在評論中指正瀏覽器

本文的代碼均由微信小遊戲自動生成的demo飛機大戰中獲取微信

文件目錄

game.js

  • 首先讓咱們來看一下做爲入口的game.js,能夠看到在這裏只進行了main類的初始化,所以下一步咱們應該查看一下main類中的函數ide

  • 代碼函數

    import Player     from './player/index'
    import Enemy      from './npc/enemy'
    import BackGround from './runtime/background'
    import GameInfo   from './runtime/gameinfo'
    import Music      from './runtime/music'
    import DataBus    from './databus'
    
    let ctx   = canvas.getContext('2d')
    let databus = new DataBus()
    
    /**
     * 遊戲主函數
     */
    export default class Main {
      constructor() {
        // 維護當前requestAnimationFrame的id
        this.aniId    = 0
        //從新生成新的界面
        this.restart()
      }
    
      //界面生成函數
      restart() {
        databus.reset()
    
        canvas.removeEventListener(
          'touchstart',
          this.touchHandler
        )
    
        this.bg       = new BackGround(ctx)
        this.player   = new Player(ctx)
        this.gameinfo = new GameInfo()
        this.music    = new Music()
    
        this.bindLoop     = this.loop.bind(this)
        this.hasEventBind = false
    
        // 清除上一局的動畫
        window.cancelAnimationFrame(this.aniId);
    
        this.aniId = window.requestAnimationFrame(
          this.bindLoop,
          canvas
        )
      }
    
      /**
       * 隨着幀數變化的敵機生成邏輯
       * 幀數取模定義成生成的頻率
       */
      enemyGenerate() {
        if ( databus.frame % 30 === 0 ) {
          let enemy = databus.pool.getItemByClass('enemy', Enemy)
          enemy.init(6)
          databus.enemys.push(enemy)
        }
      }
    
      // 全局碰撞檢測
      collisionDetection() {
        let that = this
    
        databus.bullets.forEach((bullet) => {
          for ( let i = 0, il = databus.enemys.length; i < il;i++ ) {
            let enemy = databus.enemys[i]
    
            if ( !enemy.isPlaying && enemy.isCollideWith(bullet) ) {
              enemy.playAnimation()
              that.music.playExplosion()
    
              bullet.visible = false
              databus.score  += 1
    
              break
            }
          }
        })
    
        for ( let i = 0, il = databus.enemys.length; i < il;i++ ) {
          let enemy = databus.enemys[i]
    
          if ( this.player.isCollideWith(enemy) ) {
            databus.gameOver = true
    
            break
          }
        }
      }
    
      // 遊戲結束後的觸摸事件處理邏輯
      touchEventHandler(e) {
         e.preventDefault()
    
        let x = e.touches[0].clientX
        let y = e.touches[0].clientY
    
        let area = this.gameinfo.btnArea
    
        if (   x >= area.startX
            && x <= area.endX
            && y >= area.startY
            && y <= area.endY  )
          this.restart()
      }
    
      /**
       * canvas重繪函數
       * 每一幀從新繪製全部的須要展現的元素
       */
      render() {
        ctx.clearRect(0, 0, canvas.width, canvas.height)
    
        this.bg.render(ctx)
    
        databus.bullets
              .concat(databus.enemys)
              .forEach((item) => {
                  item.drawToCanvas(ctx)
                })
    
        this.player.drawToCanvas(ctx)
    
        databus.animations.forEach((ani) => {
          if ( ani.isPlaying ) {
            ani.aniRender(ctx)
          }
        })
    
        this.gameinfo.renderGameScore(ctx, databus.score)
    
        // 遊戲結束中止幀循環
        if ( databus.gameOver ) {
          this.gameinfo.renderGameOver(ctx, databus.score)
    
          if ( !this.hasEventBind ) {
            this.hasEventBind = true
            this.touchHandler = this.touchEventHandler.bind(this)
            canvas.addEventListener('touchstart', this.touchHandler)
          }
        }
      }
    
      // 遊戲邏輯更新主函數
      update() {
        if ( databus.gameOver )
          return;
    
        this.bg.update()
    
        databus.bullets
               .concat(databus.enemys)
               .forEach((item) => {
                  item.update()
                })
    
        this.enemyGenerate()
    
        this.collisionDetection()
    
        if ( databus.frame % 20 === 0 ) {
          this.player.shoot()
          this.music.playShoot()
        }
      }
    
      // 實現遊戲幀循環
      loop() {
        databus.frame++
    
        this.update()
        this.render()
    
        this.aniId = window.requestAnimationFrame(
          this.bindLoop,
          canvas
        )
      }
    }

一點基礎知識

  • 幀:遊戲中的幀和動畫中的幀,視頻中的幀概念相似,即遊戲過程當中物體和動畫效果變化的一個週期。
  • 精靈:是遊戲中的一個基本概念,指的是在遊戲中的一個基本物體或動畫或貼圖,如NPC或者敵人,在本例中有子彈,敵機和玩家
  • 回調函數:在特定事件發生後,由事件方進行調用的函數
  • 畫布:顧名思義就是使用了畫東西的地方,其實就是用於渲染相關內容的位置

main.js

main 即爲遊戲的主函數,咱們來逐個分析一下其內容oop

  • export default 爲 ES6,即js的一個版本中的語言,在js中,任何類或對象使用export既能夠在其餘文件中經過import進行調用使用,使用 import {類或對象名} from 文件路徑,但若使用export default則能夠省略 { }, 但一份文件中僅僅能夠存在一個export default

初始化生成對象

  1. 在main函數前其調用生成了一個2d畫布,名稱爲ctx動畫

  2. 生成了一個數據總線對象databus,數據總線的內容將在下次博客中解釋this

main 類

contructor()

contructor 用於建立main 對象,其中調用了restart函數,所以咱們跳轉到restart函數中進行查看

restart()

該函數用於從新生成一個界面

  • 首先重置數據總線對象的內容

  • 監聽觸碰事件

  • 初始化背景對象,玩家對象,遊戲信息對象和音樂對象

    this.bg       = new BackGround(ctx)
    this.player   = new Player(ctx)
    this.gameinfo = new GameInfo()
    this.music    = new Music()
  • 綁定事件循環,初始化狀態,並開始運行

    this.bindLoop     = this.loop.bind(this)
        this.hasEventBind = false
    
        // 清除上一局的動畫
        window.cancelAnimationFrame(this.aniId);
    
        this.aniId = window.requestAnimationFrame(
          this.bindLoop,
          canvas
        )
  • js語法中,能夠將某個對象的方法單獨拿出來做爲一個方法使用,可是在使用過程當中,避免不了出現未知該函數所指向的對象的狀況

    • 例如在該代碼中,若寫做this.bindLoop = this.loop 那麼該函數所屬的類就丟失了,那麼該函數一些執行也就沒法進行
    • 爲了不這樣的狀況,js使用bind函數,將所需的類綁定到該函數上,這樣就有效地解決了這個問題
  • window.requestAnimationFrame()

    • 該函數使用了兩個參數,第一個是回調函數,第二個是畫布
    • 畫布的功能即用來工做的區域
    • 而回調函數的做用是在瀏覽器在該幀渲染完畢以後,調用的函數,根據博主的資料查詢,回調函數執行次數一般是每秒60次,但在大多數遵循W3C建議的瀏覽器中,回調函數執行次數一般與瀏覽器屏幕刷新次數相匹配。
    • 在該例子中,restart中的該函數僅僅是使用初始化的main對象更新loop函數,並將其做爲刷新內容
    • 但因爲main對象中的邏輯會產生變動,所以在以後的loop函數也對其進行了請求,並綁定了參數。使用新纏身過的main對象和新產生的canvas在瀏覽器中進行渲染

enemyGenerate()

該函數用於生成敵人飛機

  • 在databus中有一個frame參數,至關於每次刷新(更新)的計數器,
  • 使用該函數時,若刷新次數爲30的整數倍時,就會申請一個新的敵機對象並初始化,其中init的參數爲該敵機的速度,生成後加入databus對象的存儲數組中

collisionDetection()

全局碰撞檢測

  • 首先對於每一個子彈,判斷子彈是否與敵機相撞,若相撞則隱藏敵機和子彈
    • 該處須要解釋一下的是,將子彈和敵機隱藏的是直接表明子彈和敵機已經銷燬
    • 但此處並未在邏輯中將對象銷燬,而是在繪圖中判斷其visible是否爲true,若爲true則纔會畫入畫布中
    • 而統一更新回收入pool
  • 對每一架敵機,判斷是否與用戶相撞,若相撞,則在databus中設置遊戲結束

touchEventHandler(e)

遊戲結束後判斷是否從新開始的函數

  • 獲取觸摸的座標
  • 在gameinfo中獲取從新開始上下左右xy座標
  • 比對觸摸位置是否在按鈕內部,若在則調用restart函數從新啓動函數

render()

渲染函數,用於渲染場景,用於每次修改內容後從新渲染場景內容(每一幀調用)

  • 清除畫布的全部內容
  • 調用背景類的渲染函數,在ctx上渲染出一個背景
  • concat函數爲js函數,用於鏈接連個數組
  • 鏈接databus中的bullets和enemys數組,而且將這個合成數組中的每一項畫到畫布上,畫到畫布上的操做是以利用函數drawToCanvas,而該函數實現於Spirite類中,
  • spirit即精靈,是遊戲設計中的一個概念,至關於遊戲中一個最基本的物體或者一個概念,該demo中的spirit實現方式將在後續博客中寫上
  • 將player畫到畫布上,一樣的,player也繼承於Spirit類
  • 將全部動畫類的未播放的內容進行播放,在該demo中,Animation類繼承Spirit,而全部物體均繼承於Animation類,所以都具備該能力,不過因爲全部物體都均僅有一幀圖像,所以無需進行播放,
  • 在databus類中有一個專門存放動畫的數組,任何繼承於Animation類的對象都會在初始化構造時被放入該數組當中
  • 調用gameinfo的函數更新圖像左上角的分數內容
  • 判斷,若遊戲結束
    • 若未綁定事件,將touchHandler事件添加綁定,
    • 將事件加入監聽中
    • (該段代碼博主並未很是理解,歡迎在評論中指正或指導)

update()

遊戲邏輯更新主函數

  • 若遊戲已經結束,不執行該代碼,直接放回結束
  • 更新背景參數
  • 對全部bullets和enemys對象進行更新
  • 調用enemyGenerate() 生成敵人(根據前面描述,須要判斷是否知足恰好通過30幀)
  • 進行全局碰撞檢測,並進行處理
  • 判斷是否通過20幀,每通過20幀,調用player生成一個新的bullet(子彈),而且調用射擊音樂

loop()

實現遊戲幀循環

  • 每次循環將幀計數器加一
  • 更新邏輯
  • 渲染邏輯更新後的場景
  • 使用window.requestAnimationFrame進行調用,爲下一幀界面渲染作準備
相關文章
相關標籤/搜索