用vue3+pixijs復(fù)刻童年記憶里的游戲-獵鴨季節(jié)
大廠技術(shù)??堅(jiān)持周更??精選好文
前言
本期將用vue3與pixijs復(fù)刻出童年在小霸王里面玩的游戲-獵鴨季節(jié),當(dāng)初玩它需要光線槍才行,非常不好瞄準(zhǔn),每當(dāng)打中鴨子就非常激動(dòng),打不中就會(huì)有收到狗子的嘲笑,想想那時(shí)候快樂(lè)真的很簡(jiǎn)單?,F(xiàn)在,就想辦改造了一下,讓他不用光線槍用鼠標(biāo)也能擊中它,圓童年一個(gè)夢(mèng)。
廢話不多說(shuō),我們先來(lái)康康展示效果怎樣吧:

“演示地址:http://jsmask.gitee.io/duck-hunt/
”
介紹
因?yàn)檫@個(gè)游戲大部分界面都是用pixi.js繪制出來(lái)的,本身代碼量也是比較多的而且比較繁瑣,不可能一次全講完,所以本期主要講解一下,它里面是如何進(jìn)行加載,如何繪制界面,如何做游戲動(dòng)畫,如何進(jìn)行擊中判定,以及怎么適配屏幕尺寸等等。當(dāng)最開(kāi)始我們先來(lái)進(jìn)行一些準(zhǔn)備工作。
游戲規(guī)則
進(jìn)入游戲每輪五回合,每回合會(huì)出現(xiàn)兩只鴨子,和三發(fā)子彈。 每擊中一直鴨子則獎(jiǎng)勵(lì)500分,若每輪擊中全部鴨子則有特別獎(jiǎng)勵(lì)。 若子彈用盡或超時(shí)則鴨子都會(huì)飛走,請(qǐng)把握每一發(fā)子彈和珍惜時(shí)間。 每輪如果打中超過(guò)6只以上鴨子,才有資格晉級(jí)下一輪,最多三輪。
游戲流程
點(diǎn)擊初始畫面開(kāi)始進(jìn)入游戲界面。 每輪游戲開(kāi)始出現(xiàn)獵犬過(guò)場(chǎng)動(dòng)畫。 獵犬過(guò)場(chǎng)動(dòng)畫后,游戲可進(jìn)行射擊操作。 每次點(diǎn)擊鼠標(biāo),則表示發(fā)射子彈,點(diǎn)中鴨子則加分,點(diǎn)中鴨子,超時(shí)或子彈用光則鴨子加速飛走等邏輯。 游戲超過(guò)三輪或者未滿足晉級(jí)條件,則強(qiáng)制退回初始畫面并記錄分?jǐn)?shù),可以重新開(kāi)始。
主要技術(shù)
vite:負(fù)責(zé)整個(gè)項(xiàng)目的模塊構(gòu)建打包任務(wù)。 vue3:作為前端框架,方便完成一些界面的響應(yīng)式、組件化等。 scss:負(fù)責(zé)初始加載界面的css動(dòng)畫,與一些界面比例調(diào)整的樣式工作。 mitt.js:負(fù)責(zé)發(fā)布訂閱的任務(wù)。 pixi.js:游戲引擎,游戲中絕大部分任務(wù)都在這里完成。 gsap.js:負(fù)責(zé)一些動(dòng)畫的操作。
游戲素材
這里為了縮小游戲本體尺寸也是為了更接近原作用Press Start 2P用了像素字體,聲音統(tǒng)一做了壓縮處理從wav轉(zhuǎn)成mp3格式。而圖片原本是一張大圖這里看到的碎圖是因?yàn)橛昧薙hoeBox軟件去完成了拆分,但是我沒(méi)有做進(jìn)一步處理圖片(將它們尺寸統(tǒng)一等再進(jìn)行TexturePackerGUI拼合),后面會(huì)講明我用了另一種方法來(lái)處理這些動(dòng)畫。
開(kāi)始
發(fā)布訂閱
import?mitt?from?"mitt";
const?bus?=?{};
const?emitter?=?mitt();
bus.$on?=?emitter.on;
bus.$off?=?emitter.off;
bus.$emit?=?emitter.emit;
export?default?bus;
因?yàn)関ue3里面沒(méi)有了off,所以我們使用mitt來(lái)去代替發(fā)布訂閱這個(gè)任務(wù),里面很多的狀態(tài)改變的通知都借助于它來(lái)完成的。
文件結(jié)構(gòu)
加載動(dòng)畫組件我之前寫的一篇文章,就是拿它來(lái)完成加載動(dòng)畫的:WEB加載動(dòng)畫之像素字動(dòng)畫
DuckGame組件就是我們游戲的主容器了。
new?Game({
????width,
????height,
????el:?canvas.value,
????resolution:?1,
????onProgress:?n?=>?{
????????Bus.$emit("changeProgress",?n);
????}
}).init();
我們這里要把游戲容器傳到Game中,生成一個(gè)實(shí)例,當(dāng)然在你可以看到在onProgress中,發(fā)給加載動(dòng)畫組件通知當(dāng)前的進(jìn)度狀況。
游戲場(chǎng)景
import?{?Container?}?from?"pixi.js";
export?default?class?Scene?{
????constructor(game)?{
????????this.game?=?game;
????????this.stage?=?new?Container();
????????this.stage.interactive?=?true;
????????this.stage.buttonMode?=?true;
????????this.stage.sortableChildren?=?true
????????this.stage.zIndex?=?1
????????return?this
????}
????onStart()?{?}
????init()?{?}
????show()?{
????????this.stage.visible?=?true
????}
????hide()?{
????????this.stage.visible?=?false
????}
????update(delta)?{
????????if?(!this.stage.visible)?return;
????}
}
游戲里所有的場(chǎng)景都繼承了Scene,因?yàn)檫壿嫳容^簡(jiǎn)單只涉及了開(kāi)始界面和游戲界面兩個(gè)場(chǎng)景,所有比較的簡(jiǎn)單,目前只有顯示隱藏更新這些基礎(chǔ)方法。
我們每當(dāng)創(chuàng)建新界面就會(huì)有繼承它,如開(kāi)始界面:
import?Scene?from?"./scene"
class?StartScene?extends?Scene?{
????constructor(game)?{
????????super(game)
????????this.topScore?=?null;
????????return?this
????}
}
export?default?StartScene
加載素材
因?yàn)槲覀冇昧藇ue3所以就借雞下蛋,用了URL的方式去獲取對(duì)應(yīng)的素材。
export?function?getImageUrl(name,?ext?=?"png")?{
????return?new?URL(`/src/assets/${name}.${ext}`,?import.meta.url).href
}
然后進(jìn)行配置:
const?audioList?=?{
????fire:?getImageUrl("fire",?"mp3"),
????//?...more
}
const?stage?=?getImageUrl("stage");
//?...more
export?default?{
????stage,
????...audioList,
????//?more
}
通過(guò)pixi.js中的Loader去完成加載任務(wù),同時(shí)通知vue3加載動(dòng)畫組件當(dāng)前的加載進(jìn)度。同時(shí)還要將他們變成紋理圖存儲(chǔ)起來(lái)以方便后面pixi.js繪圖使用。
export?default?class?Game?{
????//?...
????init()?{
????????this.loaderTextures().then(res?=>?{
??????????????Object.entries(res).forEach(([key,?value])?=>?setTextures(key,?value.texture))
??????????????this.render()
????????})
????},
????loaderTextures()?{
????????const?{?loader,?onProgress?}?=?this;
????????return?new?Promise((resolve,?reject)?=>?{
??????????Object.entries(assets).forEach(([key,?value])?=>?loader.add(key,?value,?()?=>?{
????????????onProgress(loader.progress)
??????????}))
??????????loader.load((loader,?resources)?=>?{
????????????onProgress(loader.progress)
????????????resolve(resources)
??????????})
????????})
????},
????reader(){
??????//?渲染界面??
????},
????//?...
}
繪制界面
本作大部分的界面都是pixi.js中的繪圖API來(lái)完成,主要是體力勞動(dòng),可以參考pixi.js官網(wǎng)的API來(lái)學(xué)習(xí)。這里制作簡(jiǎn)單的介紹,如下面的背景黑塊繪制,和總積分繪制。
import?{?Text,?Graphics,?Container?}?from?"pixi.js";
class?StartScene?extends?Scene?{
???//?...
????drawBg()?{
????????const?{?width,?height?}?=?this.game;
????????const?graphics?=?new?Graphics();
????????graphics.beginFill(0x000000,?1);
????????graphics.drawRect(0,?0,?width,?height);
????????graphics.endFill();
????????this.stage.addChild(graphics)
????}
????drawTopScore(score?=?0)?{
????????const?{?width,?height?}?=?this.game;
????????this.topScore?=?new?Text("top?score?=?".toUpperCase()?+?score,?{
????????????fontFamily:?'Press?Start?2P',
????????????fontSize:?24,
????????????leading:?20,
????????????fill:?0x66DB33,
????????????align:?'center',
????????????letterSpacing:?4
????????});
????????this.topScore.anchor.set(0.5,?0.5);
????????this.topScore.position.set(width?/?2,?height?-?60)
????????this.stage.addChild(this.topScore)
????}
}
export?default?StartScene
游戲動(dòng)畫
因?yàn)閜ixi.js并不是一個(gè)可視化的游戲引擎,所以為了更方便的制作游戲動(dòng)畫我們使用了gsap.js來(lái)代替。游戲里會(huì)出現(xiàn)的一些閃動(dòng)的動(dòng)畫,如開(kāi)始界面中的click to start the game這段文字按鈕的閃動(dòng),利用SteppedEase緩動(dòng),看起來(lái)符合那個(gè)年代的味道。
import?{?TimelineMax?}?from?"gsap"
let?btnAni?=?new?TimelineMax().fromTo(this.btn,?{?alpha:?0?},?{?alpha:?1,?duration:?.45,?immediateRender:?true,?ease:?"SteppedEase(1)"?});
btnAni.repeat(-1)
btnAni.yoyo(true);
當(dāng)然涉及到更多的還有里面的幀動(dòng)畫,比如獵犬的搜尋,嘲笑,鴨子的飛行等等都是幀動(dòng)畫來(lái)完成的。pixi.js卻也有幀動(dòng)畫執(zhí)行的方案,但是我這里素材沒(méi)有進(jìn)一步處理所以取了個(gè)巧,還是用gsap.js的SteppedEase緩動(dòng)模擬幀,這樣的好處是每一幀都可以有方法去調(diào)節(jié)圖片的位置去彌補(bǔ)圖片大小不一產(chǎn)生的位移問(wèn)題。
let?dogSearchAni?=?new?TimelineMax()
dogSearchAni
????.from(dog,?0.16,?{?texture:?getTextures("dog0"),?ease:?"SteppedEase(1)"?})
????.to(dog,?0.16,?{?texture:?getTextures("dog1"),?ease:?"SteppedEase(1)"?})
????.to(dog,?0.16,?{?texture:?getTextures("dog2"),?ease:?"SteppedEase(1)"?})
????.to(dog,?0.16,?{?texture:?getTextures("dog3"),?ease:?"SteppedEase(1)"?})
????.to(dog,?0.2,?{?texture:?getTextures("dog4"),?ease:?"SteppedEase(1)"?})
dogSearchAni.repeat(-1)
dogSearchAni.play()

擊中判定
判定有兩種方式,第一種是包圍盒檢測(cè),判定鼠標(biāo)點(diǎn)擊的點(diǎn)是否與鴨子存在重合,若重合則表示擊中。第二種是pixi.js存在的pointerdown事件。這里偷了個(gè)懶,也防止一箭雙雕的事件產(chǎn)生就用了pointerdown事件。當(dāng)我們點(diǎn)擊到鴨子之時(shí),就改變當(dāng)前該鴨子的狀態(tài)表示擊中。同時(shí),我們的系統(tǒng)還會(huì)發(fā)出一個(gè)子彈事件,如果鴨子的isHit狀態(tài)變成true并且isDie是false表示擊中未死,那么就要執(zhí)行顯示分?jǐn)?shù),掉落死亡動(dòng)畫,最后銷毀掉。
export?default?class?Duck?{
????constructor({?dIndex?=?0,?x?=?0,?y?=?0,?speed?=?3,?direction?=?1,?stage,?rect?=?[0,?0,?1200,?759]?})?{
????????//?...
????????this.target?=?new?Container();
????????
????????//?點(diǎn)中改變狀態(tài)
????????this.target.on("pointerdown",?()?=>?{
????????????if?(!this.isHit)?this.isHit?=?true;
????????})
????????
????????//?接收子彈事件
????????Bus.$on("sendBullet",?({?e,?callback?})?=>?{
????????????if?(this.isHit?&&?!this.isDie)?{
????????????????this.isDie?=?true;
????????????????this.hit();
????????????????this.duck_sound.play()
????????????????callback?&&?callback(this)
????????????}
????????})
????????//?接收飛走事件
????????Bus.$on("flyaway",?()?=>?{
????????????this.isFlyaway?=?true;
????????})
????????return?this;
????}
????move(delta)?{
????????//?移動(dòng)
????}
????async?hit()?{
????????//?擊中
????????const?{?sprite,?score,?target?}?=?this;
????????this.normalAni.kill();
????????sprite.texture?=?getTextures("duck_9")
????????sprite.width?=?getTextures("duck_9").width
????????sprite.height?=?getTextures("duck_9").height
????????showScore({
????????????parent:?this.stage,
????????????score,
????????????x:?target.x?-?(this.vx?0???+?sprite.width?:?0),
????????????y:?target.y
????????})
????????await?wait(.35)
????????this.die()
????}
????die()?{
????????//?死亡
????}
????fly()?{
????????//?飛行
????}
????destroy()?{
????????//?銷毀
????????if?(this.target.parent)?{
????????????this.target.parent.removeChild(this.target)
????????}
????}
}
適配屏幕
為了讓界面不變形的情況下最大程度顯示出來(lái),我用了一個(gè)取巧的方案,用了css的transform:scale+v-bind的方法,讓vue計(jì)算出最大比例,然后綁定到css里面。
結(jié)語(yǔ)
總體來(lái)說(shuō),pixi.js還是非常強(qiáng)大的,處理此類的游戲再合適也不過(guò)了。如果場(chǎng)景界面非常多,動(dòng)畫也非常多的話,還是建議使用cocos creator,可以節(jié)省很多工作量。
本期的游戲也是我盡可能注意一些游戲細(xì)節(jié)做的,童年記憶從模糊也漸漸變得清晰了許多,希望各位童年也說(shuō)過(guò)我長(zhǎng)大以后要做游戲的小伙伴不忘初心,有時(shí)間把自己童年喜愛(ài)的游戲用自己的方式創(chuàng)作出來(lái),也未嘗不是一種技術(shù)上的鍛煉和追憶童年無(wú)憂無(wú)慮生活的一種方式。
???H5-Dooring,讓H5制作更簡(jiǎn)單

歡迎體驗(yàn):?http://h5.dooring.cn/h5_plus
???謝謝支持
以上便是本次分享的全部內(nèi)容,希望對(duì)你有所幫助^_^
喜歡的話別忘了?分享、點(diǎn)贊、收藏?三連哦~。
歡迎關(guān)注公眾號(hào)?趣談前端?收獲前端一手好文章~








