微信小游戲 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
初始化生成對象
-
在main函數前其調用生成了一個2d畫布,名稱為ctx
-
生成了一個數據總線對象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
進行調用,為下一幀界面渲染做准備