昨天我們為小女巫加入了自動攻擊能力,但正如我們在總結中預見的,start()
函數已經開始變得過於臃腫:它既要負責初始化背景,又要管理女巫的事件,甚至還要管理圖層、子彈物件等。
若我們繼續在這樣的架構之下添加新功能,例如敵人、碰撞傷害、UI 等等,之後它就會肥到走不動!到時候再來減肥就已經為時已晚了。
因此今天的核心目標就是透過實作 Game
類別,來扮演遊戲的中央控制器,讓每個物件各司其職,實現清晰的職責分離。
之前我們都是在 ironman2025_cook 這個專案上開發,為了分開不同天數的程式碼。從今天開始,為了專案的整潔與可維護性,我另外新創了 LittleWitch_TheJourney 專案,專門用來開發《小女巫・啟程》這個遊戲的,但我也會同步更新 ironman2025_cook 的專案,可以看到某一天的程式碼。
接著,我事先把專案的資料結構定義好了:
除了 Game
類別以外,我也將前幾天的 ParallaxBackground
、Witch
、MagicBullet
,全部獨立成各自的檔案。一個獨立的類別就獨立成一個檔案,應該算是大部分人的習慣。
接下來我們就來一個個看看我是怎麼把它們給分開的吧!
Game
類別我們的 Game
類別,同樣會繼承自 PIXI.Container
,它被視為整個遊戲的根節點。
import pixi = CG.Pixi.pixi;
import { ParallaxBackground } from './Display/ParallaxBackground';
import { Witch } from './Characters/Witch';
import { MagicBullet, IMagicBulletData } from './Effects/MagicBullet';
export class Game extends PIXI.Container {
// 視差背景
private _parallaxBackground: ParallaxBackground;
// 角色圖層
private _characterLayer: PIXI.Container;
// 特效圖層
private _effectLayer: PIXI.Container;
// 小女巫
private _witch: Witch;
constructor() {
super();
// 創建視差背景實例,並加入到舞台最底層。
const parallaxBackground = this._parallaxBackground = new ParallaxBackground();
this.addChild(parallaxBackground);
// 特效圖層(用於加入各種場景特效,如魔法彈、敵人技能等)
const effectLayer = this._effectLayer = new PIXI.Container();
this.addChild(effectLayer);
// 角色圖層(用於加入各種角色,如小女巫、敵人)
const characterLayer = this._characterLayer = new PIXI.Container();
this.addChild(characterLayer);
// 創建小女巫實例,並初始化位置等。
const witch = this._witch = new Witch();
witch.position.set(pixi.stageWidth * 0.25, pixi.stageHeight * 0.5);
characterLayer.addChild(witch);
// 接收小女巫攻擊事件,生成魔法彈並加入特效圖層
witch.on(Witch.EVENT.ATTACK, (data: IMagicBulletData) => {
const bullet = new MagicBullet(data);
effectLayer.addChild(bullet);
}, this);
// 設置更新循環函數,每一幀都呼叫 update 函數
CG.Base2.addUpdateFunction(this, this._update);
}
/**
* 更新循環函數。
* @param dt - 每幀間隔時間(ms)
*/
private _update(dt: number): void {
this._parallaxBackground.update(dt);
// 歷遍特效圖層與角色圖層的所有子物件,以執行 update()
for (const layer of [this._effectLayer, this._characterLayer]) {
const children = layer.children;
for (let i = children.length - 1; i >= 0; --i) {
const gameObject = children[i];
gameObject["update"](dt);
}
}
}
}
import
:由於我們將所有的類別拆分到各自的檔案去了,因此需要使用 import
來將它們引入進來,不過 CG 在我們使用這些類別的時候,就會自動幫我們做這件事情了。export
:想要讓其他檔案可以 import
這個類別,就必須在宣告時加上 export
關鍵字,才可以被其他檔案引入,其他的 ParallaxBackground
、Witch
等也要喔!_update(dt)
函數中,集中呼叫所有遊戲物件的 update(dt)
函數。
gameObject["update"](dt)
這裡的update
使用[""]
來讀取是為了避免 TypeScript 跳出警訊,因為它只知道children
裡面裝的是PIXI.ContainerChild[]
(容器子物件陣列),但這個物件本身並沒有update
屬性或函數,因此會跳出警訊。而我們是通過前後文知道裡面的子物件都會有update
函數,因此可以先暫時以此方式應急,這個部分應該會在實作敵人或碰撞傷害的那天做調整,暫時先稍微知道就好。
app.ts
專案進入點原先複雜的 start()
函數現在被極度簡化,它只負責載入資源和啟動單一的 Game
實例。
import pixi = CG.Pixi.pixi;
import { Game } from './Games/Game';
async function start() {
// 將專案資源載入至遊戲中,並等待載入完成。(記得將資源別名修改成你自己專案的資源別名喔!)
await pixi.assets
.add("LittleWitch_TheJourney.圖片.背景")
.add("LittleWitch_TheJourney.圖片.遠景")
.add("LittleWitch_TheJourney.圖片.前景")
.add("LittleWitch_TheJourney.圖集動畫.角色")
.add("LittleWitch_TheJourney.圖集動畫.特效")
.load();
// 初始化 Pixi。(啟用 stageMask 可以讓舞台以外的地方不會被顯示)
await pixi.initialize({ stageWidth: 960, stageHeight: 540, stageMask: true });
// 創建遊戲實例並加入舞台中。
const game = new Game();
pixi.root.addChild(game);
}
start();
stageMask
:我這次在 pixi.initialize
偷偷加了 stageMask: true
(舞台遮罩),它可以讓畫面只顯示舞台大小範圍內的東西。因為在昨天加入子彈之後,子彈是可以飛出舞台外的,沒有啟用的話就會看到子彈飛出邊界一點點。其他東西都被移入 Game
了,因此可以全部刪除,以保留 start()
的整潔。
由於獨立在各自的檔案,因此所有的類別都需要調整 import
,但其他部分就都一模一樣了。
當然還有因為切換專案,所以資源別名也需要調整,但這個我就不特別一一寫出來了。
一、ParallaxBackground
(視差背景)
import pixi = CG.Pixi.pixi;
export class ParallaxBackground extends PIXI.Container {
// ... (其餘不變)
}
二、Witch
(小女巫)
import pixi = CG.Pixi.pixi;
import keyboard = CG.Base2.keyboard;
import Key = CG.Base2.keyboards.Key;
import { IMagicBulletData } from './../Effects/MagicBullet';
export class Witch extends PIXI.Container {
// ... (其餘不變)
}
三、MagicBullet
(魔法彈)
import pixi = CG.Pixi.pixi;
// ... (其餘不變)
export class MagicBullet extends PIXI.Container {
// ... (其餘不變)
}
怎麼樣,把每個類別都各自獨立成一個檔案以後,是不是看起來就乾淨許多了?整個人都舒爽了起來~
我們今天透過實作 Game
類別,完成了專案最重要的架構重構。現在專案的職責非常明確:
app.ts
:負責載入資源與啟動遊戲。Game.ts
:負責圖層管理、物件生命週期(創建、事件監聽)和統一更新。Witch.ts
/ MagicBullet.ts
/ ParallaxBackground.ts
:只負責各自物件的內部邏輯(移動、計時、銷毀)。其實原本第二階段的一開始,Day 16 開始進入寫遊戲的階段時,我就在想要寫一個 Game
類別,但後來考慮到一開始就在寫這些畫面上看不到的東西,對於讀者來說可能會太無趣,因此便決定把最具視覺效果的視差卷軸背景放在第一天,之後再慢慢添加小女巫、魔法彈。除了讓一天的文章內容資訊量不會太過龐大,各位不好吸收以外,也剛好讓各位看到有無 Game
的前後對比,更能了解為什麼要這樣子去規劃專案架構。
有了這個穩定的架構,明天我們就可以放心地往上疊加遊戲內容。
既然小女巫已經有了穩固的飛行器和強大的火力,那麼明天就該有東西讓她打了!因此我們要來實作敵人生成系統,讓我們的第一個敵人正式從畫面右側登場!