知識
不管是網(wǎng)站,軟件還是小程序,都要直接或間接能為您產(chǎn)生價(jià)值,我們在追求其視覺表現(xiàn)的同時(shí),更側(cè)重于功能的便捷,營銷的便利,運(yùn)營的高效,讓網(wǎng)站成為營銷工具,讓軟件能切實(shí)提升企業(yè)內(nèi)部管理水平和效率。優(yōu)秀的程序?yàn)楹笃谏壧峁┍憬莸闹С郑?
您當(dāng)前位置>首頁 » 新聞資訊 » 小程序相關(guān) >
微信小游戲初體驗(yàn)
發(fā)表時(shí)間:2021-3-31
發(fā)布人:葵宇科技
瀏覽次數(shù):56
本文旨在通過分析官方給出的一個(gè)飛機(jī)大戰(zhàn)小游戲的源代碼來說明如何進(jìn)行小游戲的開發(fā)。
1.前言
前天一個(gè) 跳一跳
小游戲刷遍了朋友圈,也代表了微信小程序擁有了搭載游戲的功能(早該往這方面發(fā)展了,這才是應(yīng)該有的形態(tài)嘛)。作為一個(gè)前端er,我的大刀早已經(jīng)饑渴難耐了,趕緊去下一波最新的微信官方開發(fā)工具,體驗(yàn)一波小游戲要如何開發(fā)。
我們欣喜地看到可以直接點(diǎn)擊小游戲體驗(yàn)一下,而且官方也有一個(gè)示例源代碼,是一個(gè)簡易版的飛機(jī)大戰(zhàn)的源碼,直接點(diǎn)開模擬器就可以看效果。
2.源碼分析
(還是原汁原味的打飛機(jī)游戲呀!)通過閱讀這個(gè)源代碼我們便可以知道如何進(jìn)行小游戲的開發(fā)了。廢話少說直接進(jìn)入主題,先來分析一波源碼的整體結(jié)構(gòu)。
./js下面是官方示例中的js文件具體的作用
├── base // 定義游戲開發(fā)基礎(chǔ)類
│ ├── animatoin.js // 幀動畫的簡易實(shí)現(xiàn)
│ ├── pool.js // 對象池的簡易實(shí)現(xiàn)
│ └── sprite.js // 游戲基本元素精靈類
├── libs
│ ├── symbol.js // ES6 Symbol簡易兼容
│ └── weapp-adapter.js // 小游戲適配器
├── npc
│ └── enemy.js // 敵機(jī)類
├── player
│ ├── bullet.js // 子彈類
│ └── index.js // 玩家類
├── runtime
│ ├── background.js // 背景類
│ ├── gameinfo.js // 用于展示分?jǐn)?shù)和結(jié)算界面
│ └── music.js // 全局音效管理器
├── databus.js // 管控游戲狀態(tài)
└── main.js // 游戲入口主函數(shù)
官方文檔中提到, game.js
和 game.json
是小游戲必須要有的兩個(gè)文件
下面我會分析我認(rèn)為主要的文件與結(jié)構(gòu),不會對每一行代碼進(jìn)行解析,大家有興趣可以自行閱讀官方的源碼。每個(gè)文件后會跟隨我認(rèn)為重要的幾個(gè)小點(diǎn)。
game.js
import './js/libs/weapp-adapter'
import './js/libs/symbol'
import Main from './js/main'
new Main()
- 小程序啟動會調(diào)用
game.js
,在其中導(dǎo)入了小游戲官方提供的適配器,用于注入canvas以及模擬DOM以及BOM(后續(xù)會具體說明這個(gè)文件),可以在 https://mp.weixin.qq.com/debu...下載源代碼,修改適合自己的版本并通過webpack打包自用。當(dāng)然目前已經(jīng)足夠我們使用。 - 導(dǎo)入symbol的polyfill,主要用于模擬ES6類的私有變量。
- 導(dǎo)入Main類并實(shí)例化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ù)變化的敵機(jī)生成邏輯
* 幀數(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()
}
// 實(shí)現(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
)
}
}
- 導(dǎo)入了創(chuàng)建游戲需要的我放飛機(jī),敵方飛機(jī),背景,游戲信息,音樂,游戲全局?jǐn)?shù)據(jù)類,并獲取了canvas的上下文(看到這是不是有一個(gè)疑惑,canvas到底是從哪里定義?先帶著這個(gè)問題最后再說),創(chuàng)建了一個(gè)全局?jǐn)?shù)據(jù)實(shí)例(后面會提到)。
- 創(chuàng)建Main的實(shí)例自然會調(diào)用構(gòu)造方法,在構(gòu)造方法中調(diào)用restart函數(shù),進(jìn)行了游戲的初始化并進(jìn)行循環(huán)刷幀(
requestAnimationFrame
看起來是不是很親切)。 - loop函數(shù)中我們可以看到主要調(diào)用了update, render方法,并設(shè)置了player發(fā)射子彈的時(shí)間,對游戲是否結(jié)束進(jìn)行判斷,最后接著刷幀。
- update方法會調(diào)用各個(gè)場景內(nèi)對象的update方法來更新他們的位置以及其他信息。
- render方法會調(diào)用各個(gè)場景內(nèi)對象的render方法來將他們繪制到canvas中。
Main內(nèi)結(jié)構(gòu)清晰,主要理解整個(gè)流程就是調(diào)用 requestAnimationFrame
來不停地刷幀更新位置信息推動所有對象運(yùn)動,每個(gè)對象在每一幀都有新的位置,連起來就是動畫了。分清位置的更新與對象的繪制是關(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
}
/**
* 回收敵人,進(jìn)入對象池
* 此后不進(jìn)入幀循環(huán)
*/
removeEnemey(enemy) {
let temp = this.enemys.shift()
temp.visible = false
this.pool.recover('enemy', enemy)
}
/**
* 回收子彈,進(jìn)入對象池
* 此后不進(jìn)入幀循環(huán)
*/
removeBullets(bullet) {
let temp = this.bullets.shift()
temp.visible = false
this.pool.recover('bullet', bullet)
}
}
- 我們可以看出,databus是一個(gè)單例對象,不論在其他代碼中new多少次,都是返回的同一個(gè)實(shí)例,符合我們的期望。
- reset定義了所需要的數(shù)據(jù)源并初始化
- 通過一個(gè)對象池的概念,控制當(dāng)前頁面對象的數(shù)量,避免使用js原有的垃圾處理機(jī)制,而是通過對象池來復(fù)用已經(jīng)創(chuàng)建的對象,算是一個(gè)性能優(yōu)化。
- frame屬性主要是用來刷幀的時(shí)候用來控制子彈的發(fā)射與敵機(jī)的出現(xiàn)時(shí)間。
sprite.js
/**
* 游戲基礎(chǔ)的精靈類
*/
export default class Sprite {
constructor(imgSrc = https://www.wxapp-union.com/'', width= 0, height = 0, x = 0, y = 0) {
this.img = new Image()
this.img.src = https://www.wxapp-union.com/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
)
}
/**
* 簡單的碰撞檢測定義:
* 另一個(gè)精靈的中心點(diǎn)處于本精靈所在的矩形內(nèi)即可
* @param{Sprite} sp: Sptite的實(shí)例
*/
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上
可以看出畫圖主要是用的canvas里的drawImage方法,也是我們自行開發(fā)小游戲以后會用到的方法。包括background,player等類都會繼承自精靈類,并且會添加自己的update方法來暴露更新自己位置信息的接口。enermy還會包裝一層爆炸動畫的封裝,思路大同小異,就不在多贅述了。
3.結(jié)論
- 我們發(fā)現(xiàn)小游戲的開發(fā)與我們使用canvas進(jìn)行h5小游戲的開發(fā)并沒有什么太大的區(qū)別,無論從繪圖的api還是事件的api都十分相似,還可以用window對象,這主要?dú)w功于官方提供的
webapp-adapter.js
,該js會注入window對象并提供相應(yīng)的canvas全局變量,也是文章中提到為什么在main.js里找不到canvas變量在哪里定義的原因了。所以我們可以開開心心地使用canvas來開發(fā)小游戲了?。?! - 官方還說了一句,可以不引入
webapp-adapter.js
來開發(fā)小游戲,( https://mp.weixin.qq.com/debu... )這是小游戲的api文檔(當(dāng)時(shí)找了很久)適配器的源碼寫得也很清晰,可以一讀來了解一些,其中也有很多官方寫的TODO的事情,還并不十分完善,如果想要快速移植已有的h5游戲代碼使用適配器是很有效的。如果想直接開發(fā)小游戲根據(jù)api文檔直接來開發(fā)也是很有效的方法,畢竟引入一層適配器還是會有一定的開銷。
tips: 讀一讀適配器源碼也有利于了解如何開發(fā)小程序(例如事件綁定之類的操作)
4.結(jié)語
小程序終于可以來做小游戲了,感覺還是休閑類的游戲會占主導(dǎo)地位,前端大大可以迎接新的戰(zhàn)場啦哈哈哈~~~(接下來會去掉適配器用原生api改寫官方demo)
相關(guān)案例查看更多
相關(guān)閱讀
- 云南軟件定制
- uniapp開發(fā)小程序
- 網(wǎng)站建設(shè)案例
- 人人商城
- 正規(guī)網(wǎng)站建設(shè)公司
- 海報(bào)插件
- 云南小程序公司
- 云南網(wǎng)站建設(shè)公司哪家好
- 云南網(wǎng)站制作哪家好
- 云南建設(shè)廳網(wǎng)站
- 電商網(wǎng)站建設(shè)
- 微信小程序
- 網(wǎng)站上首頁
- 網(wǎng)站維護(hù)
- 汽車報(bào)廢軟件
- 云南etc小程序
- 汽車回收管理系統(tǒng)
- 云南網(wǎng)頁制作
- 云南小程序被騙
- 小程序技術(shù)
- 云南電商網(wǎng)站建設(shè)
- 網(wǎng)絡(luò)公司哪家好
- 軟件開發(fā)
- 網(wǎng)站開發(fā)
- 網(wǎng)站建設(shè)列表網(wǎng)
- 保山小程序開發(fā)
- 大理小程序開發(fā)
- 云南省城鄉(xiāng)建設(shè)廳網(wǎng)站
- 云南衛(wèi)視小程序
- flex