微信小游戲初體驗

本文旨在通過分析官方給出的一個飛機大戰(zhàn)小游戲的源代碼來說明如何進行小游戲的開發(fā)事扭。

1.前言

前天一個 跳一跳 小游戲刷遍了朋友圈愤钾,也代表了微信小程序擁有了搭載游戲的功能(早該往這方面發(fā)展了搪哪,這才是應(yīng)該有的形態(tài)嘛)狼忱。作為一個前端er谷醉,我的大刀早已經(jīng)饑渴難耐了操骡,趕緊去下一波最新的微信官方開發(fā)工具九火,體驗一波小游戲要如何開發(fā)。

image

我們欣喜地看到可以直接點擊小游戲體驗一下册招,而且官方也有一個示例源代碼岔激,是一個簡易版的飛機大戰(zhàn)的源碼,直接點開模擬器就可以看效果是掰。

image

2.源碼分析

(還是原汁原味的打飛機游戲呀B嵌Α)通過閱讀這個源代碼我們便可以知道如何進行小游戲的開發(fā)了。廢話少說直接進入主題键痛,先來分析一波源碼的整體結(jié)構(gòu)炫彩。

image

./js下面是官方示例中的js文件具體的作用

├── base                                   // 定義游戲開發(fā)基礎(chǔ)類
│   ├── animatoin.js                       // 幀動畫的簡易實現(xiàn)
│   ├── pool.js                            // 對象池的簡易實現(xiàn)
│   └── sprite.js                          // 游戲基本元素精靈類
├── libs
│   ├── symbol.js                          // ES6 Symbol簡易兼容
│   └── weapp-adapter.js                   // 小游戲適配器
├── npc
│   └── enemy.js                           // 敵機類
├── player
│   ├── bullet.js                          // 子彈類
│   └── index.js                           // 玩家類
├── runtime
│   ├── background.js                      // 背景類
│   ├── gameinfo.js                        // 用于展示分?jǐn)?shù)和結(jié)算界面
│   └── music.js                           // 全局音效管理器
├── databus.js                             // 管控游戲狀態(tài)
└── main.js                                // 游戲入口主函數(shù)

官方文檔中提到, game.jsgame.json 是小游戲必須要有的兩個文件

下面我會分析我認(rèn)為主要的文件與結(jié)構(gòu)絮短,不會對每一行代碼進行解析江兢,大家有興趣可以自行閱讀官方的源碼。每個文件后會跟隨我認(rèn)為重要的幾個小點丁频。

game.js

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

import Main from './js/main'

new Main()

  1. 小程序啟動會調(diào)用 game.js 杉允,在其中導(dǎo)入了小游戲官方提供的適配器,用于注入canvas以及模擬DOM以及BOM(后續(xù)會具體說明這個文件)席里,可以在 https://mp.weixin.qq.com/debu...下載源代碼叔磷,修改適合自己的版本并通過webpack打包自用。當(dāng)然目前已經(jīng)足夠我們使用奖磁。
  2. 導(dǎo)入symbol的polyfill改基,主要用于模擬ES6類的私有變量。
  3. 導(dǎo)入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()

/**
 * 游戲主函數(shù)
 */
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
    )
  }

  /**
   * 隨著幀數(shù)變化的敵機生成邏輯
   * 幀數(shù)取模定義成生成的頻率
   */
  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
      }
    }
  }

  //游戲結(jié)束后的觸摸事件處理邏輯
  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重繪函數(shù)
     * 每一幀重新繪制所有的需要展示的元素
     */
    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)
  }

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

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

    this.enemyGenerate()

    this.collisionDetection()
  }

  // 實現(xiàn)游戲幀循環(huán)
  loop() {
    databus.frame++

    this.update()
    this.render()

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

    // 游戲結(jié)束停止幀循環(huán)
    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. 導(dǎo)入了創(chuàng)建游戲需要的我放飛機,敵方飛機,背景惫霸,游戲信息矮烹,音樂,游戲全局?jǐn)?shù)據(jù)類褐啡,并獲取了canvas的上下文(看到這是不是有一個疑惑诺舔,canvas到底是從哪里定義?先帶著這個問題最后再說)备畦,創(chuàng)建了一個全局?jǐn)?shù)據(jù)實例(后面會提到)低飒。
  2. 創(chuàng)建Main的實例自然會調(diào)用構(gòu)造方法,在構(gòu)造方法中調(diào)用restart函數(shù)懂盐,進行了游戲的初始化并進行循環(huán)刷幀( requestAnimationFrame 看起來是不是很親切)褥赊。
  3. loop函數(shù)中我們可以看到主要調(diào)用了update, render方法,并設(shè)置了player發(fā)射子彈的時間莉恼,對游戲是否結(jié)束進行判斷拌喉,最后接著刷幀。
  4. update方法會調(diào)用各個場景內(nèi)對象的update方法來更新他們的位置以及其他信息俐银。
  5. render方法會調(diào)用各個場景內(nèi)對象的render方法來將他們繪制到canvas中尿背。

Main內(nèi)結(jié)構(gòu)清晰,主要理解整個流程就是調(diào)用 requestAnimationFrame 來不停地刷幀更新位置信息推動所有對象運動捶惜,每個對象在每一幀都有新的位置田藐,連起來就是動畫了。分清位置的更新與對象的繪制是關(guān)鍵吱七。

databus.js

import Pool from './base/pool'

let instance

/**
 * 全局狀態(tài)管理器
 */
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
  }

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

    temp.visible = false

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

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

    temp.visible = false

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

  1. 我們可以看出踊餐,databus是一個單例對象景醇,不論在其他代碼中new多少次,都是返回的同一個實例市袖,符合我們的期望啡直。
  2. reset定義了所需要的數(shù)據(jù)源并初始化
  3. 通過一個對象池的概念,控制當(dāng)前頁面對象的數(shù)量苍碟,避免使用js原有的垃圾處理機制酒觅,而是通過對象池來復(fù)用已經(jīng)創(chuàng)建的對象,算是一個性能優(yōu)化微峰。
  4. frame屬性主要是用來刷幀的時候用來控制子彈的發(fā)射與敵機的出現(xiàn)時間舷丹。

sprite.js

/**
 * 游戲基礎(chǔ)的精靈類
 */
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
    )
  }

  /**
   * 簡單的碰撞檢測定義:
   * 另一個精靈的中心點處于本精靈所在的矩形內(nèi)即可
   * @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方法,也是我們自行開發(fā)小游戲以后會用到的方法症概。包括background,player等類都會繼承自精靈類蕾额,并且會添加自己的update方法來暴露更新自己位置信息的接口。enermy還會包裝一層爆炸動畫的封裝彼城,思路大同小異诅蝶,就不在多贅述了。

3.結(jié)論

  1. 我們發(fā)現(xiàn)小游戲的開發(fā)與我們使用canvas進行h5小游戲的開發(fā)并沒有什么太大的區(qū)別募壕,無論從繪圖的api還是事件的api都十分相似调炬,還可以用window對象,這主要歸功于官方提供的 webapp-adapter.js 舱馅,該js會注入window對象并提供相應(yīng)的canvas全局變量缰泡,也是文章中提到為什么在main.js里找不到canvas變量在哪里定義的原因了。所以我們可以開開心心地使用canvas來開發(fā)小游戲了4汀<!
  2. 官方還說了一句资溃,可以不引入 webapp-adapter.js 來開發(fā)小游戲武翎,( https://mp.weixin.qq.com/debu... )這是小游戲的api文檔(當(dāng)時找了很久)適配器的源碼寫得也很清晰,可以一讀來了解一些溶锭,其中也有很多官方寫的TODO的事情宝恶,還并不十分完善,如果想要快速移植已有的h5游戲代碼使用適配器是很有效的趴捅。如果想直接開發(fā)小游戲根據(jù)api文檔直接來開發(fā)也是很有效的方法垫毙,畢竟引入一層適配器還是會有一定的開銷。

tips: 讀一讀適配器源碼也有利于了解如何開發(fā)小程序(例如事件綁定之類的操作)

4.結(jié)語

小程序終于可以來做小游戲了拱绑,感覺還是休閑類的游戲會占主導(dǎo)地位综芥,前端大大可以迎接新的戰(zhàn)場啦哈哈哈~~~(接下來會去掉適配器用原生api改寫官方demo)

5.無適配器版的官方demo

通過之前的源碼分析,我們只能找到使用適配器版本的官方Demo猎拨,而找不到一個無適配器版本的官方Demo膀藐,于是自己動手豐衣足食,將官方Demo的適配器移除红省,下面介紹需要進行哪些改動额各。

1、首先對適配器的源碼簡單閱讀后可以發(fā)現(xiàn)吧恃,適配器做的事情就是模擬了window對象虾啦,然后將window對象按devtool和小程序運行的實際環(huán)境暴露給全局對象,供我們來使用(devtool里就是window,實際環(huán)境中則是GameGlobal)傲醉。那么相應(yīng)我們就該把所有引用到window的地方都進行修改蝇闭,因為實際運行環(huán)境中并沒有這個全局對象。下面我主要說明在源代碼中使用到window的地方硬毕。

    • 我移除了libs/symbol.js呻引,改為直接使用原生支持的symbol來模擬私有變量,其他文件只需刪除對該文件的引入即可昭殉。
    • 查找各文件使用的window.innerHeightwindow.innerWidth 改為使用 const { screenWidth, screenHeight, devicePixelRatio } = wx.getSystemInfoSync()來獲取屏幕寬高與dpr苞七,并在相應(yīng)地方進行替換。

2挪丢、音頻文件處理

    • 主要是runtime/music.js里與小游戲api的轉(zhuǎn)化,主要是將 new Audio()轉(zhuǎn)化為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`就可以了,全局對象里已經(jīng)支持饱苟,`setInterval`一樣
    

至此我們已經(jīng)完成了移除適配器孩擂,可以在一個極簡的條件下開發(fā)我們的小游戲了!箱熬!

本文作者:kedaya

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末类垦,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子城须,更是在濱河造成了極大的恐慌蚤认,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,348評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酿傍,死亡現(xiàn)場離奇詭異烙懦,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評論 2 385
  • 文/潘曉璐 我一進店門氯析,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亏较,“玉大人,你說我怎么就攤上這事掩缓⊙┣椋” “怎么了?”我有些...
    開封第一講書人閱讀 156,936評論 0 347
  • 文/不壞的土叔 我叫張陵你辣,是天一觀的道長巡通。 經(jīng)常有香客問我,道長舍哄,這世上最難降的妖魔是什么宴凉? 我笑而不...
    開封第一講書人閱讀 56,427評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮表悬,結(jié)果婚禮上弥锄,老公的妹妹穿的比我還像新娘。我一直安慰自己蟆沫,他們只是感情好籽暇,可當(dāng)我...
    茶點故事閱讀 65,467評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著饭庞,像睡著了一般戒悠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上舟山,一...
    開封第一講書人閱讀 49,785評論 1 290
  • 那天绸狐,我揣著相機與錄音,去河邊找鬼捏顺。 笑死六孵,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的幅骄。 我是一名探鬼主播劫窒,決...
    沈念sama閱讀 38,931評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拆座!你這毒婦竟也來了主巍?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,696評論 0 266
  • 序言:老撾萬榮一對情侶失蹤挪凑,失蹤者是張志新(化名)和其女友劉穎孕索,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體躏碳,經(jīng)...
    沈念sama閱讀 44,141評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡搞旭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,483評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肄渗。...
    茶點故事閱讀 38,625評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡镇眷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出翎嫡,到底是詐尸還是另有隱情欠动,我是刑警寧澤,帶...
    沈念sama閱讀 34,291評論 4 329
  • 正文 年R本政府宣布惑申,位于F島的核電站具伍,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏圈驼。R本人自食惡果不足惜人芽,卻給世界環(huán)境...
    茶點故事閱讀 39,892評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望碗脊。 院中可真熱鬧啼肩,春花似錦、人聲如沸衙伶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽矢劲。三九已至,卻和暖如春慌随,著一層夾襖步出監(jiān)牢的瞬間芬沉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工阁猜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留丸逸,地道東北人。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓剃袍,卻偏偏與公主長得像黄刚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子民效,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,492評論 2 348

推薦閱讀更多精彩內(nèi)容