|
本文旨在通過分析官方給出的一個飛機大戰(zhàn)小游戲的源代碼來說明如何進行小游戲的開發(fā)。 1.前言前天一個 跳一跳 小游戲刷遍了朋友圈,也代表了微信小程序擁有了搭載游戲的功能(早該往這方面發(fā)展了,這才是應該有的形態(tài)嘛)。作為一個前端er,我的大刀早已經(jīng)饑渴難耐了,趕緊去下一波最新的微信官方開發(fā)工具,體驗一波小游戲要如何開發(fā)。
我們欣喜地看到可以直接點擊小游戲體驗一下,而且官方也有一個示例源代碼,是一個簡易版的飛機大戰(zhàn)的源碼,直接點開模擬器就可以看效果。
2.源碼分析(還是原汁原味的打飛機游戲呀?。┩ㄟ^閱讀這個源代碼我們便可以知道如何進行小游戲的開發(fā)了。廢話少說直接進入主題,先來分析一波源碼的整體結構。
├── base // 定義游戲開發(fā)基礎類 │ ├── 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 // 用于展示分數(shù)和結算界面 │ └── music.js // 全局音效管理器 ├── databus.js // 管控游戲狀態(tài) └── main.js // 游戲入口主函數(shù) 官方文檔中提到, game.js 和 game.json 是小游戲必須要有的兩個文件 下面我會分析我認為主要的文件與結構,不會對每一行代碼進行解析,大家有興趣可以自行閱讀官方的源碼。每個文件后會跟隨我認為重要的幾個小點。 game.jsimport './js/libs/weapp-adapter' import './js/libs/symbol' import Main from './js/main' new Main()
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
}
}
}
//游戲結束后的觸摸事件處理邏輯
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()
}
// 游戲結束停止幀循環(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
)
}
}
Main內(nèi)結構清晰,主要理解整個流程就是調(diào)用 requestAnimationFrame 來不停地刷幀更新位置信息推動所有對象運動,每個對象在每一幀都有新的位置,連起來就是動畫了。分清位置的更新與對象的繪制是關鍵。 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)
}
}
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
)
}
/**
* 簡單的碰撞檢測定義:
* 另一個精靈的中心點處于本精靈所在的矩形內(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 )
}
}
可以看出畫圖主要是用的canvas里的drawImage方法,也是我們自行開發(fā)小游戲以后會用到的方法。包括background,player等類都會繼承自精靈類,并且會添加自己的update方法來暴露更新自己位置信息的接口。enermy還會包裝一層爆炸動畫的封裝,思路大同小異,就不在多贅述了。 3.結論
tips: 讀一讀適配器源碼也有利于了解如何開發(fā)小程序(例如事件綁定之類的操作) 4.結語小程序終于可以來做小游戲了,感覺還是休閑類的游戲會占主導地位,前端大大可以迎接新的戰(zhàn)場啦哈哈哈~~~(接下來會去掉適配器用原生api改寫官方demo) |