首先,要開發 web game,通常有兩種選項:
- 與開發一般網頁無異,直接操作 DOM 以及 CSS 來製作遊戲,較適合不需要大量動態的遊戲,e.g., 2048
- 使用 HTML5 的 canvas 來渲染出遊戲,適合需要大量動態的遊戲,較容易實現動畫以及各種 filter 特效等等
這個系列我們主要會以 2 的方式來開發,因為最後的目標成品,一個橫向卷軸遊戲會包含主角移動、物理系統、敵人、各種動畫以及特效等等。
而為了快速上手,整個系列會使用 PixiJS 作為 canvas 繪圖的 library,畢竟直接操作 canvas 的原生 API,會需要冗長的程式碼,開發體驗會差很多。
那麼說到瑪利歐這種橫向卷軸遊戲,最重要的莫過於主人公了,沒有紅色水管工的瑪利歐,就像沒有牛肉的牛肉湯麵,根本無法下嚥,因此,透過此篇文章,創造屬於你自己的主人公吧!
PixiJS Setup
首先,先建立一個 canvas element
<canvas id="camvas"></canvas>
接下來在 head 裡加入 PixiJS 的 CDN,然後創建 PixiJS 的主體(PIXI.Application
),之後會用到的元件都會在它底下,包含最核心的 game loop(PIXI.Ticker
)、放置所有遊戲物件的 stage(PIXI.Container
)、載入各種 assets 的 loader(PIXI.Loader
)以及 screen、renderer 等等。
const canvas = document.querySelector('#canvas');
const pixi = new PIXI.Application({
view: canvas,
});
Demo on CodePen
畫出你的主人公吧
為了快速簡單以及教學目的,我們先使用 primitive shapes(長方形、圓形、三角形等等) 來建構我們的遊戲世界。
我喜歡粉紅色,就用可愛的粉紅正方形做我的主人公吧,pinky square!
首先,先設定一些主人公的長相參數:
const protagonistWidth = 100;
const protagonistEyeRadius = 10;
const protagonistEyesDist = 40; // 兩眼距離
再來,創建一個群組物件(PIXI.Container
)來代表我們的主人公
const protagonist = new PIXI.Container();
這時候,有人就會問:為什麼不直接畫主人公的身體、眼睛、鼻子、嘴巴等等就好?
因為若是把他們當成分開的物件,實作上會造成許多困擾,例如:
- 位置定位:所有物件的位置都要相對於原點(螢幕左上角)來做設定,無法以主人公的原點(例如:主人公的頭頂)來做定位,會造成開發體驗不佳
- transform:若想要套用 transform 在整個主人公上,不管是 scale, rotate, skew,就必須同時移動他的身體、眼睛、鼻子、嘴巴等等,不切實際
- 移動:若想要移動整個主人公時,就必須同時移動他的身體、眼睛、鼻子、嘴巴等等,非常麻煩
- 等等各種問題...
而意義上,主人公的身體、眼睛、鼻子、嘴巴等等都是主人公的一部份,所以主人公應該要是一個包含他們的群組物件才符合邏輯。
如此以來,實作上,不會浪費多餘的運算及操作,並享有良好開發體驗,意義上,也提升了程式碼的可讀性。
接下來,畫出主人公的身體:
const protagonistBody = new PIXI.Graphics();
protagonistBody.beginFill(0xffb6b9);
protagonistBody.drawRoundedRect(0, 0, protagonistWidth, protagonistWidth, 20);
protagonistBody.endFill();
然後馬上將身體加入主人公群組:
protagonist.addChild(protagonistBody);
這裡是關鍵,群組(PIXI.Container
)的長寬範圍是以它包含的子物件(child)的邊界為準,也就是說在沒有包含任何子物件的情況下,它的長寬皆為 0。
假設想要讓主人公的鼻子在身體的中間,在群組沒有包含任何子物件的情形下,const protagonistNoseX = protagonist.width / 2;
,protagonistNoseX
會是 0,結果就是鼻子會在身體的最左邊。
所以通常來說,只要創立了一個群組,就必須馬上加入一個子物件作為基底,來確保之後所有相對於群組物件長寬的運算都會是正確的。
以主人公的例子來說,因為我確定主人公的身體就會是代表整個主人公群組物件邊界的子物件,不會有任何其他子物件超過身體的範圍,才會以它作為基底。
當然,目前群組中的子物件都是靜態的,所以基底的選擇相對簡單,若子物件為遊戲過程中動態產生,那麼基底的決定就會複雜許多,這個進階議題會在之後的系列文再做討論。
畫出主人公的眼睛:
const protagonistEyes = new PIXI.Graphics();
protagonistBody.beginFill(0x000000);
protagonistBody.drawCircle(protagonist.width / 2 - protagonistEyesDist / 2, 30, protagonistEyeRadius);
protagonistBody.drawCircle(protagonist.width / 2 + protagonistEyesDist / 2, 30, protagonistEyeRadius);
protagonistBody.endFill();
再來設定主人公的錨點(pivot)以及位置:
protagonist.pivot.set(protagonist.width / 2, protagonist.height);
protagonist.position.set(pixi.screen.width / 2, pixi.screen.height);
錨點的意思就是物件定位的基準點,舉例來說,這裡我設定主人公的錨點是主人公身體的中間下方,主人公的位置則設定為螢幕的中間下方,所以主人公的中間下方就會與螢幕的中間下方對準,這是一個非常方便定位的功能,記得善加利用。
最後把主人公加進stage
,讓主人公顯示在螢幕上:
pixi.stage.addChild(protagonist);
主人公的外型方面,你可以使用任何你喜歡的顏色,甚至要加鼻子、嘴巴等等都可以,但身體部分的形狀請先務必使用方形(包含長方形及正方形),因為在之後的物理碰撞系統,方形的實作會較為容易,當然非方形也是可以,但是是較為進階的主題,實作也會複雜許多。