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


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

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

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

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

本博客將使用逐行代碼分析的方式講解該demo,本文適用於對其他高級語言熟悉,對js還未深入了解的同學,博主會盡可能將所有遇到的不明白的部分標注清楚,若有不正確或不清楚的地方,歡迎在評論中指正

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

文件目錄

game.js

  • 首先讓我們來看一下作為入口的game.js,可以看到在這里只進行了main類的初始化,因此下一步我們應該查看一下main類中的函數

  • 代碼

    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 即為游戲的主函數,我們來逐個分析一下其內容

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

初始化生成對象

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

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

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進行調用,為下一幀界面渲染做准備


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM