摘要:前言前天一個(gè)跳一跳小游戲刷遍了朋友圈,也代表了微信小程序擁有了搭載游戲的功能早該往這方面發(fā)展了,這才是應(yīng)該有的形態(tài)嘛。作為一個(gè)前端,我的大刀早已經(jīng)饑渴難耐了,趕緊去下一波最新的微信官方開發(fā)工具,體驗(yàn)一波小游戲要如何開發(fā)。
本文旨在通過分析官方給出的一個(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è)簡(jiǎn)易版的飛機(jī)大戰(zhàn)的源碼,直接點(diǎn)開模擬器就可以看效果。
2.源碼分析(還是原汁原味的打飛機(jī)游戲呀?。┩ㄟ^閱讀這個(gè)源代碼我們便可以知道如何進(jìn)行小游戲的開發(fā)了。廢話少說直接進(jìn)入主題,先來分析一波源碼的整體結(jié)構(gòu)。
路徑 | 內(nèi)容 |
---|---|
audio | 音頻文件目錄 |
images | 圖片文件目錄 |
js | 主要源代碼目錄 |
game.js | 游戲主入口 |
game.json | 游戲的配置文件 |
下面是官方示例中的js文件具體的作用
./js ├── base // 定義游戲開發(fā)基礎(chǔ)類 │ ├── animatoin.js // 幀動(dòng)畫的簡(jiǎn)易實(shí)現(xiàn) │ ├── pool.js // 對(duì)象池的簡(jiǎn)易實(shí)現(xiàn) │ └── sprite.js // 游戲基本元素精靈類 ├── libs │ ├── symbol.js // ES6 Symbol簡(jiǎn)易兼容 │ └── 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è)文件
下面我會(huì)分析我認(rèn)為主要的文件與結(jié)構(gòu),不會(huì)對(duì)每一行代碼進(jìn)行解析,大家有興趣可以自行閱讀官方的源碼。每個(gè)文件后會(huì)跟隨我認(rèn)為重要的幾個(gè)小點(diǎn)。
import "./js/libs/weapp-adapter" import "./js/libs/symbol" import Main from "./js/main" new Main()
小程序啟動(dòng)會(huì)調(diào)用game.js,在其中導(dǎo)入了小游戲官方提供的適配器,用于注入canvas以及模擬DOM以及BOM(后續(xù)會(huì)具體說明這個(gè)文件),可以在https://mp.weixin.qq.com/debu... 下載源代碼,修改適合自己的版本并通過webpack打包自用。當(dāng)然目前已經(jīng)足夠我們使用。
導(dǎo)入symbol的polyfill,主要用于模擬ES6類的私有變量。
導(dǎo)入Main類并實(shí)例化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ù)變化的敵機(jī)生成邏輯 * 幀數(shù)取模定義成生成的頻率 */ enemyGenerate() { if ( databus.frame % 30 === 0 ) { let enemy = databus.pool.getItemByClass("enemy", Enemy) enemy.init(6) databus.enemys.push(enemy) } } // 全局碰撞檢測(cè) 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í)例(后面會(huì)提到)。
創(chuàng)建Main的實(shí)例自然會(huì)調(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í)間,對(duì)游戲是否結(jié)束進(jìn)行判斷,最后接著刷幀。
update方法會(huì)調(diào)用各個(gè)場(chǎng)景內(nèi)對(duì)象的update方法來更新他們的位置以及其他信息。
render方法會(huì)調(diào)用各個(gè)場(chǎng)景內(nèi)對(duì)象的render方法來將他們繪制到canvas中。
Main內(nèi)結(jié)構(gòu)清晰,主要理解整個(gè)流程就是調(diào)用requestAnimationFrame來不停地刷幀更新位置信息推動(dòng)所有對(duì)象運(yùn)動(dòng),每個(gè)對(duì)象在每一幀都有新的位置,連起來就是動(dòng)畫了。分清位置的更新與對(duì)象的繪制是關(guān)鍵。
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)入對(duì)象池 * 此后不進(jìn)入幀循環(huán) */ removeEnemey(enemy) { let temp = this.enemys.shift() temp.visible = false this.pool.recover("enemy", enemy) } /** * 回收子彈,進(jìn)入對(duì)象池 * 此后不進(jìn)入幀循環(huán) */ removeBullets(bullet) { let temp = this.bullets.shift() temp.visible = false this.pool.recover("bullet", bullet) } }
我們可以看出,databus是一個(gè)單例對(duì)象,不論在其他代碼中new多少次,都是返回的同一個(gè)實(shí)例,符合我們的期望。
reset定義了所需要的數(shù)據(jù)源并初始化
通過一個(gè)對(duì)象池的概念,控制當(dāng)前頁(yè)面對(duì)象的數(shù)量,避免使用js原有的垃圾處理機(jī)制,而是通過對(duì)象池來復(fù)用已經(jīng)創(chuàng)建的對(duì)象,算是一個(gè)性能優(yōu)化。
frame屬性主要是用來刷幀的時(shí)候用來控制子彈的發(fā)射與敵機(jī)的出現(xiàn)時(shí)間。
/** * 游戲基礎(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 ) } /** * 簡(jiǎn)單的碰撞檢測(cè)定義: * 另一個(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 ) } }
作為所有場(chǎng)景對(duì)象的基類,定義了所有精靈對(duì)象基本有的信息(位置,圖片,是否可見)
定義了兩種能力,檢測(cè)碰撞與將自己繪制在canvas上
可以看出畫圖主要是用的canvas里的drawImage方法,也是我們自行開發(fā)小游戲以后會(huì)用到的方法。包括background,player等類都會(huì)繼承自精靈類,并且會(huì)添加自己的update方法來暴露更新自己位置信息的接口。enermy還會(huì)包裝一層爆炸動(dòng)畫的封裝,思路大同小異,就不在多贅述了。3.結(jié)論
我們發(fā)現(xiàn)小游戲的開發(fā)與我們使用canvas進(jìn)行h5小游戲的開發(fā)并沒有什么太大的區(qū)別,無論從繪圖的api還是事件的api都十分相似,還可以用window對(duì)象,這主要?dú)w功于官方提供的webapp-adapter.js,該js會(huì)注入window對(duì)象并提供相應(yīng)的canvas全局變量,也是文章中提到為什么在main.js里找不到canvas變量在哪里定義的原因了。所以我們可以開開心心地使用canvas來開發(fā)小游戲了?。。?/p>
官方還說了一句,可以不引入webapp-adapter.js來開發(fā)小游戲,(https://mp.weixin.qq.com/debu...)這是小游戲的api文檔(當(dāng)時(shí)找了很久)適配器的源碼寫得也很清晰,可以一讀來了解一些,其中也有很多官方寫的TODO的事情,還并不十分完善,如果想要快速移植已有的h5游戲代碼使用適配器是很有效的。如果想直接開發(fā)小游戲根據(jù)api文檔直接來開發(fā)也是很有效的方法,畢竟引入一層適配器還是會(huì)有一定的開銷。
tips: 讀一讀適配器源碼也有利于了解如何開發(fā)小程序(例如事件綁定之類的操作)
4.結(jié)語小程序終于可以來做小游戲了,感覺還是休閑類的游戲會(huì)占主導(dǎo)地位,前端大大可以迎接新的戰(zhàn)場(chǎng)啦哈哈哈~~~(接下來會(huì)去掉適配器用原生api改寫官方demo)
12.30更新
5.無適配器版的官方demo通過之前的源碼分析,我們只能找到使用適配器版本的官方Demo,而找不到一個(gè)無適配器版本的官方Demo,于是自己動(dòng)手豐衣足食,將官方Demo的適配器移除,下面介紹需要進(jìn)行哪些改動(dòng)。
首先對(duì)適配器的源碼簡(jiǎn)單閱讀后可以發(fā)現(xiàn),適配器做的事情就是模擬了window對(duì)象,然后將window對(duì)象按devtool和小程序運(yùn)行的實(shí)際環(huán)境暴露給全局對(duì)象,供我們來使用(devtool里就是window,實(shí)際環(huán)境中則是GameGlobal)。那么相應(yīng)我們就該把所有引用到window的地方都進(jìn)行修改,因?yàn)閷?shí)際運(yùn)行環(huán)境中并沒有這個(gè)全局對(duì)象。下面我主要說明在源代碼中使用到window的地方。
我移除了libs/symbol.js,改為直接使用原生支持的symbol來模擬私有變量,其他文件只需刪除對(duì)該文件的引入即可。
查找各文件使用的window.innerHeight與window.innerWidth 改為使用 const { screenWidth, screenHeight, devicePixelRatio } = wx.getSystemInfoSync()來獲取屏幕寬高與dpr,并在相應(yīng)地方進(jìn)行替換。
音頻文件處理
主要是runtime/music.js里與小游戲api的轉(zhuǎn)化,主要是將 new Audio()轉(zhuǎn)化為wx.createInnerAudioContext()方法獲取實(shí)例和currentTime在原生是一個(gè)只讀屬性,要改為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()獲取實(shí)例即可
canvas對(duì)象處理
因?yàn)樾枰直┞叮晕覀儼裞anvas歸于到Databus全局管理中去,使用wx.createCanvas()獲取全局canvas對(duì)象
export default class DataBus { constructor() { if ( instance ) return instance instance = this this.pool = new Pool() this.canvas = wx.createCanvas() this.reset() } }
事件機(jī)制
canvas對(duì)象沒有addEventListener之類的方法,同理BOM和DOM對(duì)象都沒有,所以需要用微信的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就可以了,全局對(duì)象里已經(jīng)支持,setInterval一樣
至此我們已經(jīng)完成了移除適配器,可以在一個(gè)極簡(jiǎn)的條件下開發(fā)我們的小游戲了??!
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/90538.html
摘要:準(zhǔn)備工作開發(fā)工具下載地址竟然隱藏在一個(gè)超鏈接里,真的很不醒目啊文檔地址初次體驗(yàn)新建項(xiàng)目的時(shí)候,可以選擇是小程序小游戲,這次我體驗(yàn)的是小程序的開發(fā)??梢耘渲眯〕绦蜻吔绲模珥敳康膶?dǎo)航欄的一些樣式。 很早前就想體驗(yàn)一把小程序的開發(fā)了,如果熟悉三大框架的話,小程序的開發(fā)還是很容易上手的,所以只要跟著做一個(gè)小應(yīng)用,加上官方的文檔就十分好學(xué)了。加上官方提供的開發(fā)工具也比較簡(jiǎn)潔好用,初次體驗(yàn)的我覺...
摘要:下面我主要使用了小程序標(biāo)簽的方法對(duì)輸入的值進(jìn)行監(jiān)聽,然后進(jìn)行正則匹配。標(biāo)簽微信小程序標(biāo)簽自帶屬性,可以調(diào)起帶有小數(shù)點(diǎn)的數(shù)字鍵盤,屬性可以控制我們輸入字符的個(gè)數(shù),然后我們給標(biāo)簽綁定方法。 開篇廢話 在開發(fā)過程中經(jīng)常遇到這樣的需求:用戶只能輸入數(shù)字并且只保留小數(shù)點(diǎn)兩位。雖然我們可以在提交表單的時(shí)候進(jìn)行驗(yàn)證,但是體驗(yàn)不是很好。下面我主要使用了小程序input標(biāo)簽的bindinput方法對(duì)輸入...
摘要:下面我主要使用了小程序標(biāo)簽的方法對(duì)輸入的值進(jìn)行監(jiān)聽,然后進(jìn)行正則匹配。標(biāo)簽微信小程序標(biāo)簽自帶屬性,可以調(diào)起帶有小數(shù)點(diǎn)的數(shù)字鍵盤,屬性可以控制我們輸入字符的個(gè)數(shù),然后我們給標(biāo)簽綁定方法。 開篇廢話 在開發(fā)過程中經(jīng)常遇到這樣的需求:用戶只能輸入數(shù)字并且只保留小數(shù)點(diǎn)兩位。雖然我們可以在提交表單的時(shí)候進(jìn)行驗(yàn)證,但是體驗(yàn)不是很好。下面我主要使用了小程序input標(biāo)簽的bindinput方法對(duì)輸入...
閱讀 5036·2021-07-25 21:37
閱讀 683·2019-08-30 15:53
閱讀 3350·2019-08-29 18:47
閱讀 687·2019-08-29 15:39
閱讀 2130·2019-08-29 13:12
閱讀 1798·2019-08-29 12:43
閱讀 2978·2019-08-26 11:52
閱讀 1890·2019-08-26 10:15