H5遊戲開發:FC小蜜蜂
前言
說起任天堂 FC 那是充滿我們童年寒暑假的回憶,那時候沒有正版紅白機,玩的是幾十塊一台的山寨小霸王,十塊一張的卡帶,玩著魂斗羅、馬里奧、淘金者、快打旋風、打鴨子等等。
進入正題,今天我們來說說怎麼做一個 FC 小蜜蜂遊戲,遊戲玩法是通過操控飛機,通過發射子彈對蜜蜂造成傷害,蜜蜂全部殲滅則視為勝利。
初始化
本次遊戲採用 Phaser 引擎進行開發,Phaser 是一個快速、免費、易於維護的開源 2D 遊戲框架,支持 JavaScript 和 TypeScript 兩種語言開發,採用 Pixi.js 引擎作為底層渲染,內置了物理引擎、粒子動畫、骨骼動畫等效果。
在 Phaser 中有一個重要的概念,我們需要通過狀態(State)來管理遊戲中各個不同的場景,這也是 Phaser 官方建議的遊戲代碼組織方式,場景可以通過 `Phaser.Game.state` 來添加(add)和啟動(start),每個場景有初始化(init)、預載入(preload)、準備就緒(create)、更新周期(update)、渲染完畢(render) 五種狀態,按照順序依次執行,同一時間只能存在一個場景,並且每個場景中至少包含五種狀態中的一個。
比如我們的小蜜蜂遊戲一共會分為四個場景:開始場景、遊戲場景、獲勝場景、失敗場景
var game = new Phaser.Game(750, 1206, Phaser.AUTO, wrapper)var states = {}states.start = { // 開始場景 preload: function() { ... game.load.image(example-1, images/example-1.png) ... }, create: function() { game.state.start(play) // 載入完成後切換到遊戲場景 }}states.play = { ... } // 遊戲場景states.victory = { ... } // 勝利場景states.defeat = { ... } // 失敗場景game.state.add(start, states.load)game.state.add(play, states.play)game.state.add(victory, states.victory)game.state.add(defeat states.defeat)game.state.start(start)
無限滾屏
在無限滾屏中,遊戲背景沿著 x 軸或者 y 軸重複的滾動,從而實現飛機一直在向前飛的錯覺,我們通過創建兩個背景,分別初始定位到一屏和二屏的位置,在繪製(update)的過程中持續移動兩張背景圖的 y 軸,當監聽到兩個背景超出特定位置後重新定位,從而達到無限循環背景的效果。
var bg1 = game.add.image(0, 0, background), bg2 = game.add.image(0, -bg1.height, background)update: function() { // 持續的移動 bg1.y += 2 bg2.y += 2 // 超出屏幕判斷 if (bg1.y >= 1206) { bg1.y = 0 } if (bg2.y >= 0) { bg2.y = -bg1.height }}
當然,還有更為簡便的方式,Phaser 提供了 TileSprite 平鋪紋理,非常適合於這類平鋪的背景,再結合 autoScroll() 方法,兩行代碼解決,另外還有一種叫 TileMaps 平鋪的瓦片地圖,很適合製作 FC 馬里歐這類遊戲,以後有機會再開一篇文章講講。
var bg = game.add.tileSprite(0, 0, 750, 1206, background)bg.autoScroll(0, 200) // 水平滾動速度、垂直滾動速度
創建一架飛機
飛機的移動我們通過鍵盤方向鍵進行控制,通過修改 x、y 值來實現位移,為了有更好的靈活性,我們使用 vx 和 vy 來控制 Sprite 的移動,vx 用於設置 Sprite 在 x 軸上的速度和方向,vy 用於設置 Sprite 在 y 軸上的速度和方向,不直接修改 Sprite 的 x 和 y 值,而是先更新速度值,然後再將這些速度值分配給 Sprite。
...airplane.vx = 0airplane.vy = 0update: function() { airplane.x += airplane.vx airplane.y += airplane.vy}...
接著監聽鍵盤事件,需要注意的就是在彈起狀態的時候要判斷反向的鍵是否也已經彈起,避免造成互相干擾。
...left.onDown.add(function() { airplane.vx = -8 })left.onUp.add(function() { if (!right.isDown) { airplane.vx = 0 } })...
最後是限制飛機的移動範圍,我們要限制飛機只在屏幕範圍內移動,類似空氣牆效果,通過持續監聽飛機上下左右四個方向是否碰觸到邊緣,對坐標進行歸位,具體實現代碼請看 contain 方法。
生成子彈
在遊戲中,我們需要不斷的發射子彈,這就存在一個問題,如何管理子彈?
因為子彈越多會越佔用我們的內存,遊戲會發現越來越卡,我們使用對象池的方式生成子彈,並且在子彈擊中蜜蜂或者超出屏幕時進行銷毀。
對象池的本質是復用,通過 Group 和 getFirstExists 來實現。在優化前,我們每次創建子彈都會 new Sprite,使用一次後就丟掉,優化後是創建子彈後會放入對象池中,每次使用從對象池中取,如果對象池中有則使用對象池中的子彈。
this.bullets = game.add.group() // 創建對象池var bullet = this.bullets.getFirstExists(false) // 從對象池中取非存活狀態的子彈if (bullet) { // 對象池中存在則復用 bullet.reset(this.airplane.x + 16, this.airplane.y - 20)} else { // 對象池中不存在則創建一個放入對象池中 bullet = game.add.sprite(this.airplane.x + 27, this.airplane.y - 15, bullet) this.bullets.addChild(bullet)}
創建一群蜜蜂
整體移動
創建 5 x 5 小蜜蜂是採用 Group 將所有的小蜜蜂對象放入其中,持續移動 Group,檢測 Group 左右是否碰壁,進行反方向移動,但你會發現小蜜蜂的左右的某一列被殲滅後,Group 的寬度會隨著小蜜蜂列數的變化而變化,而 Group 的 X 軸坐標還是以原來的寬度輸出 X 坐標,這就導致我們在計算碰撞牆壁的時候出現問題。
因此我們改為通過 Group 來控制整體移動,小蜜蜂負責碰撞檢測,當檢測到小蜜蜂碰撞後,進行反方向移動,並跳出循環。
for (var i = 0; i < galaxians.length; i++) { var cur = galaxians[i] if (cur.x + cur.parent.x < 0 || cur.x + cur.parent.x + cur.width > game.world.width) { // 反向移動 break }}
隨機自殺式襲擊
在間隔一段時間後隨機小蜜蜂發起攻擊,間隔不採用 setInterval 的方式,因為 setInterval 即使在頁面最小化或非激活狀態依然執行,我們採用 Phaser 提供的 Time 進行間隔觸發避免此問題。
game.time.events.loop(Phaser.Timer.SECOND * 1.5, function() { // 每兩秒隨機一隻小蜜蜂 var now = galaxians[(Math.floor(Math.random() * galaxians.length)]})
如何計算小蜜蜂向飛機發起攻擊的運動軌跡,這裡要藉助三角函數的力量來解決,通過飛機位置和蜜蜂位置,獲得對邊(a)和鄰邊(b)的長度,根據勾股定理求出斜邊(c)長度,知道各邊長度後就能得到三角比。另外有一點,Group 的 X 軸在持續的移動,小蜜蜂會受 Group 影響,所以在移動小蜜蜂時要注意。
var a = airplane.x + airplane.width / 2 - now.x + now.width /2 // 獲取 a 邊長度var b = airplane.y + airplane.height / 2 - now.y + now.height / 2 // 獲取 b 邊長度var c = Math.sqrt(a * a + b * b) // 求出斜邊 c 長度var speedX = a / c * 8var speedY = b / c * 8now.x += speedXnow.y += speedY
碰撞檢測
在遊戲中,我們需要檢測子彈與蜜蜂的碰撞和檢測蜜蜂與飛機的碰撞,在 2D 遊戲中,常用的有軸對齊包圍盒(簡稱 AABB)就是一個每條邊都平行於 X 軸或者 Y 軸的矩形。
AABB 可以用兩個點表示:最大點和最小點,在 2D 中,最小點就是左下角的點,而最大點則是右上角的點。
通過判斷 AABB 與 AABB 是否有存在交叉即可得知是否有碰撞。
function hitTestRectangle(a, b) { var hit = (a.max.x < b.min.x) || (b.max.x < a.min.x) || (a.max.y < b.min.y) || (b.max.y < a.min.y) { return !hit }}
以上就是 AABB 與 AABB 碰撞檢測的原理,當然,你也可以省事採用 Phaser 提供的物理引擎,在 Phaser 中內置了三種物理引擎,分別是:Arcade Physics、P2 Physics 和 Ninja Physics。
Arcade Physics:是三個中最為簡單、性能最快的物理引擎,因為它的碰撞都是採用 AABB 與 AABB 的碰撞,所有的碰撞都是基於一個矩形邊界(hitbox)來計算的,所有如果你想碰撞一個圓形的 Sprite,碰撞的則是它的矩形邊界,而不是圓形本身,並且支持摩擦力、重力、彈跳、加速等物理效果,適合應用於精度要求不高,較為簡單的遊戲中。
P2 Physics:它是一個更為複雜和逼真的物理引擎,使用 P2 你可以創建彈簧、鐘擺、馬達等東西,它唯一的缺點在於運算量大,對於性能有較高的要求。
Ninja Physics:比 Arcade Physics 要複雜一點,最初是為 Flash 遊戲而創造的,而現在由 Phaser 的作者 Richard Davey 移植到 JavaScript,它與其他物理引擎最大的區別在於支持斜坡碰撞。
下面簡單介紹一下 Arcade Physics 的使用方法,首先要啟動物理引擎
game.physics.startSystem(Phaser.Physics.ARCADE);
接著是需要為每個對象開啟物理效果,顯然一個個創建、添加對象並不高效,我更建議的是通過 Group 的形式添加,這樣在 Group 上創建的對象都可以開啟物理效果。
game.physics.arcade.enable(airplane) // 單獨開啟方式var platforms = game.add.group()platforms.enableBody = true // 組開啟方式platforms.create(0, 0, airplane)
完成這些以後就可以在 update 階段使用碰撞檢測,overlap 方法可傳入兩個遊戲對象,對象可以是 Sprites、Groups 或者 Emitters,可以執行 Sprite 與 Sprite、Sprite 與 Group、Group 與 Group 的碰撞檢測,與 collide 方法不同,該方法的物體不會執行任何的物理效果,它只負責碰撞檢測。
update: function() { game.physics.arcade.overlap(object1, object2, overlapCallback, processCallback, callbackContext)}
到此碰撞檢測介紹就到這,關於物理引擎的更多使用方法可移步至官網查看。
體驗地址
【點擊這裡體驗】鍵盤方向鍵控制移動,空格發射子彈,暫時只支持 PC 端體驗,另外遊戲還有很多可增加的功能,比如:關卡設計(蜜蜂血量、速度、分數)、蜜蜂發射子彈、蜜蜂貝塞爾曲線移動、蜜蜂歸位、音樂音效、爆炸動畫等等。
尾巴
如果你希望入門 H5 遊戲開發,不妨拿這個練練手,源碼你可以在體驗地址中查看到,Phaser 是很適合作為你入門 H5 遊戲開發的一款遊戲引擎,等你熟練使用也希望你能閱讀源碼,了解其中的原理,本文較為簡單,感謝你的閱讀。
我們會定期更新關於「H5遊戲開發」的文章,歡迎關注我們的知乎專欄。
參考資料
kittykatattack/learningPixi
photonstorm/phaser
Setting up Ninja Physics in Phaser
《遊戲編程演算法與技巧》(Sanjay,Madhav)【摘要 書評 試讀】- 京東圖書
推薦閱讀: