Vite + Phaser 3 打磚塊小遊戲 (下)

Lastor
Code 隨筆放置場
7 min readSep 30, 2021

承接上一篇【Webpack 簡易打包與 Eva.js 試用】,繼續來玩玩其他 H5 遊戲框架。這次使用 Phaser 3 以及最近前端比較紅的 Vite 進行打包。

主要是跟著 MDN 教學製作的一款打磚塊小遊戲,整理了一些紀錄與心得,最終寫完的內容長度有點超出預期,所以分成上、下兩篇。

上篇集中分享 Vite 的初見心得,下篇則是打磚塊小遊戲的歷程。
1. Vite + Phaser 3 打磚塊小遊戲(上)
2. Vite + Phaser 3 打磚塊小遊戲(下)

最終完成的程式碼,也先附上。
1. Breakout Game (no-framework)
2. Breakout Game with Phaser 3 + Vite

由於主要是心得筆記性質,所以不會講得太鉅細靡遺。

MDN 打磚塊小遊戲

MDN tutorials 上面有一個關於 2D Web Game 的系列,其中打磚塊這條目又分成原生 JS 版本,以及 Phaser 框架版本。個人是兩邊都跟著走了一遍,並且嘗試加了一些自己的想法。

然而,Phaser 版本的教學,年代有點過於久遠,MDN 使用的是 Phaser 2,現在則已經發展到了 Phaser 3 版本,兩邊落差是有點巨大的。所幸,另外有搜尋到其他人改寫的 Phaser 3 版本的教學。

  1. 2D breakout game — MDN tutorials
  2. MDN Breakout with Phaser 3 — Michael Bragg

接下來進入心得分享環節。

ball 的反彈算法

最開始我以為球的反彈,可能得用 ball 運動的直線向量去算甚麼內、外積之類,求出入射角之後,再去反轉鏡像什麼的,光想就覺得好複雜。

結果 MDN 給出的思路非常單純,單獨定義出 x 與 y 方向的速度,將其設為同速,這樣 ball 最初會往右上方跑,呈現一個斜率為 1,45度的直線運動。

// ball config
let dx = 2
let dy = -2

接下來,撞到左右方的東西時,就反轉 x 方向的正負號,撞到垂直方向的東西,就反轉 y 方向的正負,這樣一來撞擊反彈的效果就出來了,不需要做太複雜的物理計算,很高明的 idea。

Canvas 2D 繪製基本圖形

Cavnas 2D 繪製基本圖形的寫法,蠻有意思的。

// 繪製矩形
ctx.beginPath()
ctx.rect(posX, posY, width, height)
ctx.fillStyle = '#0095DD'
ctx.fill()
ctx.closePath()

這操作方式,看著各種熟悉,這不就是在 Photoshop 中,繪製路徑、框一個矩形出來、填滿路徑,一模一樣的操作嗎!? 瞬間有種打通任督二脈的感覺。

Remake 重製

跟著 MDN 跑完一次之後,就興起了想徹底重構一次,加點功能的念頭,所以另外做了一個 remake 版本,主要使用 Class 的寫法進行重構,將這遊戲的基本元素球與方塊,各自用 OOP 概念,做成 Circle 與 Rectangle Class,讓它們可以 instance 複用。

MDN 對於 Game Over 的處理是比較暴力的,直接 reload 網頁。我試著取消這種方式,另外加入了 Opening 與 Ending 兩個 Scene,使其 play 過程能形成一個完整的環。但這樣處理確實會麻煩很多,得多進行遊戲參數與場景的重置,還有 listener 回收之類的處理。

然後 MDN 有做一個命數機制的玩意,有 3 條命,存有命數的時候死亡會重置 paddle 與 ball 的位置直接重新開始,但這最終 UX 呈現看著會以為是 bug。再加上記憶中以前玩過的打磚塊是沒有命數機制的,所以 remake 版我直接拿掉了。

再來是 ball 的運動軌跡,MDN 只有單純的反轉水平、垂直方向的運動,這樣 ball 的運動軌跡永遠都是一條 45 度的斜線,且速率一致,顯得有點單調。

因此我試著做了一點處理,將 paddle 切成三個區塊,分成遠、中、近。接著判斷球撞擊到哪個區塊,就對 x 方向加速 or 減速,這樣一來就可以多出些變化,達成一種偽物理的效果。

Window.requestAnimationFrame 的潛在問題

要在瀏覽器上做 animation 相關的東西,原理上必須弄個機制,讓每個 frame 都執行一個 callback,這樣我們才能透過這個 callback 去控制動畫元件。

// move ball's posX and posY per frame
const fps = 60
setInterval(() => {
ball.x += 2
ball.y += -2
}, 1000 / fps)

要做出這個機制,最簡單的方法莫過於使用 setInterval() 掛一個計時器上去。另一個則是使用瀏覽器的 API Window.requestAnimationFrame(),讓瀏覽器去決定何時要執行 callback。

由於瀏覽器除了運行 JS之外,還要處理很多其他的工作,所以讓瀏覽器去決定何時要 draw frame,而不是讓 JS 用 setInterval() 去爭瀏覽器的運算資源,鐵定是比較理想的。

Window.requestAnimationFrame() 其實有個坑,就是它的 fps 會依裝置不同而有所差異,例如電腦 A 是 60fps,而電腦 B 則是 120fps,那同樣的寫法在這兩台電腦上就會出現 ball 的移動速度差一倍的問題。

搜尋一下之後才第一次知道,瀏覽器的刷新頻率是跟著螢幕的更新率去跑的,如果螢幕是 60Hz,那瀏覽器就會盡可能的以 60fps 的速度去調用 Window.requestAnimationFrame()

這個目前還不知道有甚麼好的解決方式,只能徒手去設置 startTime 跟 currentTime,手動計算去將高更新率的裝置,限制到較低更新率,才能統一不同裝置的動畫速度。

具體的做法這邊就不多花篇幅介紹,有興趣的可以我完成的 remake 版,裡頭的 GameApp.animate() method。

考量到這點,或許直接用 setInterval() 反而更簡單一點。

關於 Phaser

體驗完原生 Canvas API 的版本之後,接下來嘗試引入 Phaser 遊戲框架,原則上是繼續跟著 MDN 的教學走。

框架的好處自然很多,許多輪子不用自己造,物理、碰撞那些都有包好的函式,像上述那種 fps 的問題,多半框架也都幫忙處理掉了,就像使用 jQuery 一定程度上不用擔心瀏覽器差異的問題。

MDN 上的教學是 Phaser 2,但現在 Phaser 已經發展到 v3 版本了,其中差異還蠻巨大的,要自行看著 v2 的教學,改寫成 v3 版實屬有點難度。所幸,查到了一篇用 Phaser 3 改寫 MDN 打磚塊教學的文章,才得以繼續進行。

Phaser 整體用下來,最直接的感受是,這真的是個遊戲引擎,內容很龐大,相較 Vue、React 完全是另一個世界。很像是把 Maya 或是 3ds Max 那種大型軟體的 GUI 拔掉,弄成純 code 版本,真的要玩到比較溜恐怕得花不少時間成本。

然而,現在已經不是 Web Game 的時代了,邊做邊 google 的過程可以明顯感受出社群體量、討論熱度比起 Web 前後端的東西小很多,遇到問題要查解法會花比較多的時間。

Phaser 官方有提供一系列單一功能的 demo,去那邊找需要的功能會比翻 API 文件要快的多。可能會需要再跟一次 Phaser 官方的一些教學資源,才能更清楚這框架的結構,以及找資源的方式。

框架感想大致上就這些吧,這一塊單純是跟著教學跑一遍,畢竟同一個東西寫第三次,沒有力氣再去重構,也就沒甚麼特別的東西可以分享了。

--

--

Lastor
Code 隨筆放置場

Web Frontend / 3D Modeling / Game and Animation. 設計本科生,前遊戲業 3D Artist,專擅日本動畫與遊戲相關領域。現在轉職為前端工程師,以專業遊戲美術的角度涉足 Web 前端開發。