本文旨在經過分析官方給出的一個飛機大戰小遊戲的源代碼來講明如何進行小遊戲的開發。
前天一個跳一跳
小遊戲刷遍了朋友圈,也表明了微信小程序擁有了搭載遊戲的功能(早該往這方面發展了,這纔是應該有的形態嘛)。做爲一個前端er,個人大刀早已經飢渴難耐了,趕忙去下一波最新的微信官方開發工具,體驗一波小遊戲要如何開發。javascript
咱們欣喜地看到能夠直接點擊小遊戲體驗一下,並且官方也有一個示例源代碼,是一個簡易版的飛機大戰的源碼,直接點開模擬器就能夠看效果。html
(仍是原汁原味的打飛機遊戲呀!)經過閱讀這個源代碼咱們即可以知道如何進行小遊戲的開發了。廢話少說直接進入主題,先來分析一波源碼的總體結構。前端
路徑 | 內容 |
---|---|
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.js
和game.json
是小遊戲必需要有的兩個文件
下面我會分析我認爲主要的文件與結構,不會對每一行代碼進行解析,你們有興趣能夠自行閱讀官方的源碼。每一個文件後會跟隨我認爲重要的幾個小點。webpack
import './js/libs/weapp-adapter' import './js/libs/symbol' import Main from './js/main' new Main()
game.js
,在其中導入了小遊戲官方提供的適配器,用於注入canvas以及模擬DOM以及BOM(後續會具體說明這個文件),能夠在https://mp.weixin.qq.com/debu... 下載源代碼,修改適合本身的版本並經過webpack打包自用。固然目前已經足夠咱們使用。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 ) } }
requestAnimationFrame
看起來是否是很親切)。
Main內結構清晰,主要理解整個流程就是調用
requestAnimationFrame
來不停地刷幀更新位置信息推進全部對象運動,每一個對象在每一幀都有新的位置,連起來就是動畫了。分清位置的更新與對象的繪製是關鍵。
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) } }
/** * 遊戲基礎的精靈類 */ 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 ) } }
能夠看出畫圖主要是用的canvas裏的drawImage方法,也是咱們自行開發小遊戲之後會用到的方法。包括background,player等類都會繼承自精靈類,而且會添加本身的update方法來暴露更新本身位置信息的接口。enermy還會包裝一層爆炸動畫的封裝,思路大同小異,就不在多贅述了。
webapp-adapter.js
,該js會注入window對象並提供相應的canvas全局變量,也是文章中提到爲何在main.js裏找不到canvas變量在哪裏定義的緣由了。因此咱們能夠開開心心地使用canvas來開發小遊戲了!!!webapp-adapter.js
來開發小遊戲,(https://mp.weixin.qq.com/debu...)這是小遊戲的api文檔(當時找了好久)適配器的源碼寫得也很清晰,能夠一讀來了解一些,其中也有不少官方寫的TODO的事情,還並不十分完善,若是想要快速移植已有的h5遊戲代碼使用適配器是頗有效的。若是想直接開發小遊戲根據api文檔直接來開發也是頗有效的方法,畢竟引入一層適配器仍是會有必定的開銷。tips: 讀一讀適配器源碼也有利於瞭解如何開發小程序(例如事件綁定之類的操做)web
小程序終於能夠來作小遊戲了,感受仍是休閒類的遊戲會佔主導地位,前端大大能夠迎接新的戰場啦哈哈哈~~~(接下來會去掉適配器用原生api改寫官方demo)json
12.30更新canvas
經過以前的源碼分析,咱們只能找到使用適配器版本的官方Demo,而找不到一個無適配器版本的官方Demo,因而本身動手豐衣足食,將官方Demo的適配器移除,下面介紹須要進行哪些改動。小程序
首先對適配器的源碼簡單閱讀後能夠發現,適配器作的事情就是模擬了window對象,而後將window對象按devtool和小程序運行的實際環境暴露給全局對象,供咱們來使用(devtool裏就是window,實際環境中則是GameGlobal)。那麼相應咱們就該把全部引用到window的地方都進行修改,由於實際運行環境中並無這個全局對象。下面我主要說明在源代碼中使用到window的地方。微信小程序
libs/symbol.js
,改成直接使用原生支持的symbol來模擬私有變量,其餘文件只需刪除對該文件的引入便可。window.innerHeight
與window.innerWidth
改成使用 const { screenWidth, screenHeight, devicePixelRatio } = wx.getSystemInfoSync()
來獲取屏幕寬高與dpr,並在相應地方進行替換。音頻文件處理
主要是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() } }
圖片文件的處理
new Image()
替換爲wx.createImage()
獲取實例便可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() } }
事件機制
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)) }
requestAnimationFrame
方法
window
就能夠了,全局對象裏已經支持,setInterval
同樣至此咱們已經完成了移除適配器,能夠在一個極簡的條件下開發咱們的小遊戲了!!