微信小遊戲初體驗

本文旨在經過分析官方給出的一個飛機大戰小遊戲的源代碼來講明如何進行小遊戲的開發。

1.前言

前天一個跳一跳小遊戲刷遍了朋友圈,也表明了微信小程序擁有了搭載遊戲的功能(早該往這方面發展了,這纔是應該有的形態嘛)。做爲一個前端er,個人大刀早已經飢渴難耐了,趕忙去下一波最新的微信官方開發工具,體驗一波小遊戲要如何開發。javascript

微信開發者工具最新版

咱們欣喜地看到能夠直接點擊小遊戲體驗一下,並且官方也有一個示例源代碼,是一個簡易版的飛機大戰的源碼,直接點開模擬器就能夠看效果。html

官方源碼飛機大戰遊戲

2.源碼分析

(仍是原汁原味的打飛機遊戲呀!)經過閱讀這個源代碼咱們即可以知道如何進行小遊戲的開發了。廢話少說直接進入主題,先來分析一波源碼的總體結構。前端

路徑 內容
audio 音頻文件目錄
images 圖片文件目錄
js 主要源代碼目錄
game.js 遊戲主入口
game.json 遊戲的配置文件

下面是官方示例中的js文件具體的做用java

./js
├── base                                   // 定義遊戲開發基礎類
│   ├── animatoin.js                       // 幀動畫的簡易實現
│   ├── pool.js                            // 對象池的簡易實現
│   └── sprite.js                          // 遊戲基本元素精靈類
├── libs
│   ├── symbol.js                          // ES6 Symbol簡易兼容
│   └── weapp-adapter.js                   // 小遊戲適配器
├── npc
│   └── enemy.js                           // 敵機類
├── player
│   ├── bullet.js                          // 子彈類
│   └── index.js                           // 玩家類
├── runtime
│   ├── background.js                      // 背景類
│   ├── gameinfo.js                        // 用於展現分數和結算界面
│   └── music.js                           // 全局音效管理器
├── databus.js                             // 管控遊戲狀態
└── main.js                                // 遊戲入口主函數
官方文檔中提到, game.jsgame.json是小遊戲必需要有的兩個文件

下面我會分析我認爲主要的文件與結構,不會對每一行代碼進行解析,你們有興趣能夠自行閱讀官方的源碼。每一個文件後會跟隨我認爲重要的幾個小點。webpack

game.js
import './js/libs/weapp-adapter'
import './js/libs/symbol'

import Main from './js/main'

new Main()
  1. 小程序啓動會調用game.js,在其中導入了小遊戲官方提供的適配器,用於注入canvas以及模擬DOM以及BOM(後續會具體說明這個文件),能夠在https://mp.weixin.qq.com/debu... 下載源代碼,修改適合本身的版本並經過webpack打包自用。固然目前已經足夠咱們使用。
  2. 導入symbol的polyfill,主要用於模擬ES6類的私有變量。
  3. 導入Main類並實例化Main,因而順藤摸瓜咱們將目光移至Main.js
Main.js
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() {
    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()

    window.requestAnimationFrame(
      this.loop.bind(this),
      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)
  }

  // 遊戲邏輯更新主函數
  update() {
    this.bg.update()

    databus.bullets
           .concat(databus.enemys)
           .forEach((item) => {
              item.update()
            })

    this.enemyGenerate()

    this.collisionDetection()
  }

  // 實現遊戲幀循環
  loop() {
    databus.frame++

    this.update()
    this.render()

    if ( databus.frame % 20 === 0 ) {
      this.player.shoot()
      this.music.playShoot()
    }

    // 遊戲結束中止幀循環
    if ( databus.gameOver ) {
      this.gameinfo.renderGameOver(ctx, databus.score)

      this.touchHandler = this.touchEventHandler.bind(this)
      canvas.addEventListener('touchstart', this.touchHandler)

      return
    }

    window.requestAnimationFrame(
      this.loop.bind(this),
      canvas
    )
  }
}
  1. 導入了建立遊戲須要的我放飛機,敵方飛機,背景,遊戲信息,音樂,遊戲全局數據類,並獲取了canvas的上下文(看到這是否是有一個疑惑,canvas究竟是從哪裏定義?先帶着這個問題最後再說),建立了一個全局數據實例(後面會提到)。
  2. 建立Main的實例天然會調用構造方法,在構造方法中調用restart函數,進行了遊戲的初始化並進行循環刷幀(requestAnimationFrame看起來是否是很親切)。
  3. loop函數中咱們能夠看到主要調用了update, render方法,並設置了player發射子彈的時間,對遊戲是否結束進行判斷,最後接着刷幀。
  4. update方法會調用各個場景內對象的update方法來更新他們的位置以及其餘信息。
  5. render方法會調用各個場景內對象的render方法來將他們繪製到canvas中。
Main內結構清晰,主要理解整個流程就是調用 requestAnimationFrame來不停地刷幀更新位置信息推進全部對象運動,每一個對象在每一幀都有新的位置,連起來就是動畫了。分清位置的更新與對象的繪製是關鍵。
databus.js
import Pool from './base/pool'

let instance

/**
 * 全局狀態管理器
 */
export default class DataBus {
  constructor() {
    if ( instance )
      return instance

    instance = this

    this.pool = new Pool()

    this.reset()
  }

  reset() {
    this.frame      = 0
    this.score      = 0
    this.bullets    = []
    this.enemys     = []
    this.animations = []
    this.gameOver   = false
  }

  /**
   * 回收敵人,進入對象池
   * 此後不進入幀循環
   */
  removeEnemey(enemy) {
    let temp = this.enemys.shift()

    temp.visible = false

    this.pool.recover('enemy', enemy)
  }

  /**
   * 回收子彈,進入對象池
   * 此後不進入幀循環
   */
  removeBullets(bullet) {
    let temp = this.bullets.shift()

    temp.visible = false

    this.pool.recover('bullet', bullet)
  }
}
  1. 咱們能夠看出,databus是一個單例對象,不論在其餘代碼中new多少次,都是返回的同一個實例,符合咱們的指望。
  2. reset定義了所須要的數據源並初始化
  3. 經過一個對象池的概念,控制當前頁面對象的數量,避免使用js原有的垃圾處理機制,而是經過對象池來複用已經建立的對象,算是一個性能優化。
  4. frame屬性主要是用來刷幀的時候用來控制子彈的發射與敵機的出現時間。
sprite.js
/**
 * 遊戲基礎的精靈類
 */
export default class Sprite {
  constructor(imgSrc = '', width=  0, height = 0, x = 0, y = 0) {
    this.img     = new Image()
    this.img.src = imgSrc

    this.width  = width
    this.height = height

    this.x = x
    this.y = y

    this.visible = true
  }

  /**
   * 將精靈圖繪製在canvas上
   */
  drawToCanvas(ctx) {
    if ( !this.visible )
      return

    ctx.drawImage(
      this.img,
      this.x,
      this.y,
      this.width,
      this.height
    )
  }

  /**
   * 簡單的碰撞檢測定義:
   * 另外一個精靈的中心點處於本精靈所在的矩形內便可
   * @param{Sprite} sp: Sptite的實例
   */
  isCollideWith(sp) {
    let spX = sp.x + sp.width / 2
    let spY = sp.y + sp.height / 2

    if ( !this.visible || !sp.visible )
      return false

    return !!(   spX >= this.x
              && spX <= this.x + this.width
              && spY >= this.y
              && spY <= this.y + this.height  )
  }
}
  1. 做爲全部場景對象的基類,定義了全部精靈對象基本有的信息(位置,圖片,是否可見)
  2. 定義了兩種能力,檢測碰撞與將本身繪製在canvas上
能夠看出畫圖主要是用的canvas裏的drawImage方法,也是咱們自行開發小遊戲之後會用到的方法。包括background,player等類都會繼承自精靈類,而且會添加本身的update方法來暴露更新本身位置信息的接口。enermy還會包裝一層爆炸動畫的封裝,思路大同小異,就不在多贅述了。

3.結論

  1. 咱們發現小遊戲的開發與咱們使用canvas進行h5小遊戲的開發並無什麼太大的區別,不管從繪圖的api仍是事件的api都十分類似,還能夠用window對象,這主要歸功於官方提供的webapp-adapter.js,該js會注入window對象並提供相應的canvas全局變量,也是文章中提到爲何在main.js裏找不到canvas變量在哪裏定義的緣由了。因此咱們能夠開開心心地使用canvas來開發小遊戲了!!!
  2. 官方還說了一句,能夠不引入webapp-adapter.js來開發小遊戲,(https://mp.weixin.qq.com/debu...)這是小遊戲的api文檔(當時找了好久)適配器的源碼寫得也很清晰,能夠一讀來了解一些,其中也有不少官方寫的TODO的事情,還並不十分完善,若是想要快速移植已有的h5遊戲代碼使用適配器是頗有效的。若是想直接開發小遊戲根據api文檔直接來開發也是頗有效的方法,畢竟引入一層適配器仍是會有必定的開銷。

tips: 讀一讀適配器源碼也有利於瞭解如何開發小程序(例如事件綁定之類的操做)web

4.結語

小程序終於能夠來作小遊戲了,感受仍是休閒類的遊戲會佔主導地位,前端大大能夠迎接新的戰場啦哈哈哈~~~(接下來會去掉適配器用原生api改寫官方demo)json


12.30更新canvas

5.無適配器版的官方demo

經過以前的源碼分析,咱們只能找到使用適配器版本的官方Demo,而找不到一個無適配器版本的官方Demo,因而本身動手豐衣足食,將官方Demo的適配器移除,下面介紹須要進行哪些改動。小程序

  1. 首先對適配器的源碼簡單閱讀後能夠發現,適配器作的事情就是模擬了window對象,而後將window對象按devtool和小程序運行的實際環境暴露給全局對象,供咱們來使用(devtool裏就是window,實際環境中則是GameGlobal)。那麼相應咱們就該把全部引用到window的地方都進行修改,由於實際運行環境中並無這個全局對象。下面我主要說明在源代碼中使用到window的地方。微信小程序

    • 我移除了libs/symbol.js,改成直接使用原生支持的symbol來模擬私有變量,其餘文件只需刪除對該文件的引入便可。
    • 查找各文件使用的window.innerHeightwindow.innerWidth 改成使用 const { screenWidth, screenHeight, devicePixelRatio } = wx.getSystemInfoSync()來獲取屏幕寬高與dpr,並在相應地方進行替換。
  2. 音頻文件處理

    • 主要是runtime/music.js裏與小遊戲api的轉化,主要是將 new Audio()轉化爲wx.createInnerAudioContext()方法獲取實例和currentTime在原生是一個只讀屬性,要改成seek方法

      let instance
      
      export default class Music {
        constructor() {
          if ( instance )
            return instance
      
          instance = this
      
          // this.bgmAudio = new Audio()
          this.bgmAudio      = wx.createInnerAudioContext()
          this.bgmAudio.loop = true
          this.bgmAudio.src  = 'audio/bgm.mp3'
      
          // this.shootAudio     = new Audio()
          this.bgmAudio      = wx.createInnerAudioContext()
          this.shootAudio.src = 'audio/bullet.mp3'
      
          // this.boomAudio     = new Audio()
          this.bgmAudio      = wx.createInnerAudioContext()
          this.boomAudio.src = 'audio/boom.mp3'
      
          this.playBgm()
        }
      
        playBgm() {
          this.bgmAudio.play()
        }
      
        playShoot() {
          // this.shootAudio.currentTime = 0
          this.boomAudio.seek(0)
          this.shootAudio.play()
        }
      
        playExplosion() {
          // this.boomAudio.currentTime = 0
          this.boomAudio.seek(0)
          this.boomAudio.play()
        }
      }
  3. 圖片文件的處理

    • 與音頻文件相似,將new Image()替換爲wx.createImage()獲取實例便可
  4. canvas對象處理

    • 由於須要全局暴露,因此咱們把canvas歸於到Databus全局管理中去,使用wx.createCanvas()獲取全局canvas對象

      export default class DataBus {
          constructor() {
          if ( instance )
            return instance
      
          instance = this
      
          this.pool = new Pool()
          this.canvas = wx.createCanvas()
          this.reset()
        }
       }
  5. 事件機制

    • canvas對象沒有addEventListener之類的方法,同理BOM和DOM對象都沒有,因此須要用微信的api來處理事件,demo裏則是換爲wx.onTouchStart() wx.onTouchMove() wx.onTouchEnd()替換先有的方法。(注意main.js裏也有須要替換的,原理同樣,不贅述了)

      // player/index.js
      initEvent() {
      wx.onTouchStart(((e) => {
        let x = e.touches[0].clientX
        let y = e.touches[0].clientY
      
        //
        if (this.checkIsFingerOnAir(x, y)) {
          this.touched = true
      
          this.setAirPosAcrossFingerPosZ(x, y)
        }
      
      }).bind(this))
      wx.onTouchMove(((e) => {
      
        let x = e.touches[0].clientX
        let y = e.touches[0].clientY
      
        if (this.touched)
          this.setAirPosAcrossFingerPosZ(x, y)
      
      }).bind(this))
      
      wx.onTouchEnd(((e) => {
        this.touched = false
      }).bind(this))
      }
  6. requestAnimationFrame方法

    • 去掉前面的window就能夠了,全局對象裏已經支持,setInterval同樣

至此咱們已經完成了移除適配器,能夠在一個極簡的條件下開發咱們的小遊戲了!!

相關文章
相關標籤/搜索