iT邦幫忙

2025 iThome 鐵人賽

DAY 18
1

昨天,我們成功讓小女巫學會了自由飛行。但一位優秀的巫師在戰鬥中只會閃躲可不行!今天我們要為她裝備火力,實作她的核心戰鬥機制——自動攻擊,發射她的第一顆魔法彈!

先說說我們今天要做的事情:

  • 實作 MagicBullet 類別:讓子彈能夠移動並在飛出畫面時自動銷毀。
  • Witch 類別發射子彈:設定計時器,讓女巫定期發出攻擊事件。
  • 主循環整合:在 start() 函數中監聽事件並生成魔法彈,並確保所有魔法彈都能在主循環中更新位置。

▸ 程式碼實作:MagicBullet 魔法彈類別

我們將魔法彈的行為(移動、銷毀)封裝在一個 MagicBullet 類別中,讓它能像昨天的 Witch 一樣獨立運作。

// 定義生成魔法彈時需要的資料
interface IMagicBulletData {
	pos: { x: number, y: number };
	dir: { x: number, y: number };
	scale: number;
}

class MagicBullet extends PIXI.Container {

	// 魔法彈的 Sprite 物件
	private _sprite: PIXI.Sprite;

	constructor(private _data: IMagicBulletData) {
		super();

		// 從特效圖集中取得魔法彈紋理
		const texture = pixi.assets.getSpritesheet("ironman2025_cook.圖集動畫.特效").textures["magic_bullet"];
		const spirte = this._sprite = new PIXI.Sprite({
			texture: texture,
			anchor: 0.5,
			scale: 0.5 // 這個素材圖片稍大,預設縮小成 0.5
		});
		this.addChild(spirte);

		// 將子彈的初始位置設定為發射點
		this.position.copyFrom(_data.pos);
		this.scale.set(0);

		// 讓魔法彈生成時有一個微小的縮放淡入動畫,增加視覺效果
		new TWEEN.Tween(this.scale).to({ x: _data.scale, y: _data.scale }, 50)
			.easing(TWEEN.Easing.Cubic.Out).start();
	}

	/**
	 * 負責魔法彈的移動與銷毀邏輯。
	 */
	public update(dt: number): void {

		const { dir } = this._data;
		// 1. 根據方向向量和 dt 更新位置
		this.x += dir.x * dt;
		this.y += dir.y * dt;

		// 2. 邊界檢查與銷毀(當子彈飛出畫面時)
		const width = pixi.stageWidth;
		const height = pixi.stageHeight;
		// 取得子彈的半寬/半高
		const halfWidth = this._sprite.width * this.scale.x * 0.5;
		const halfHeight = this._sprite.height * this.scale.y * 0.5;

		// 若魔法彈超出任意邊界時銷毀 (使用半寬/高來確保整個子彈離開畫面)
		if (this.x < -halfWidth ||
			this.x > width + halfWidth ||
			this.y < -halfHeight ||
			this.y > height + halfHeight) {
			// 呼叫 destroy 釋放記憶體
			this.destroy({ children: true });
		}
	}

}
  • IMagicBulletData:這是一個 TypeScript 的 Interface,用於規範生成魔法彈時必須傳入的數據結構。這樣做可以清晰地定義子彈的「出生參數」,當我在創建 MagicBullet 實例時,程式編輯器(IDE)可以更好的告訴我應該傳入什麼參數。
  • 資源管理:與昨天的小女巫圖片相同,我打算將所有的特效圖放在 "ironman2025_cook.圖集動畫.特效" 裡面整合,雖然目前只有魔法彈,不過之後應該也會再多一些敵方的攻擊特效、技能等等。
  • 動畫效果:我在中加入了一個微小的 Tween,讓子彈生成時會由小放大,而不是憑空出現,以提升發射子彈時的視覺效果。
  • 自動銷毀:在 update() 函數中,子彈會檢查自己的位置,一旦飛出畫面邊界,就立即呼叫 this.destroy() 銷毀自己,避免記憶體洩漏。

▸ 讓 Witch 定期發射魔法彈

// 承接 Day 17 的 Witch 類別,只顯示新增的部分

class Witch extends PIXI.Container {

	// 定義靜態事件名稱,方便使用
	static readonly EVENT = {
		ATTACK: "witch_attack"
	}

	// 攻擊間隔時間(毫秒)
	private _attackInterval: number = 1000;
	// 攻擊計時器(單位:毫秒)
	private _attackTimer: number = 0;
	// 子彈速度
	private _bulletSpeed = { x: 0.3, y: 0 };

	// ... (constructor 和其他屬性不變)

	/**
	 * 發射魔法彈。
	 */
	private _launchMagicBullet(): void {
		this.emit(Witch.EVENT.ATTACK, {
			// 將魔法彈的發射位置偏移至掃帚的頭部
			pos: { x: this.x + 50, y: this.y + 31 },
			dir: this._bulletSpeed,
			scale: 1
		} as IMagicBulletData);
	}

	update(dt: number): void {
		// ... (移動邏輯不變)

		// 1. 攻擊計時
		this._attackTimer += dt;
		if (this._attackTimer >= this._attackInterval) {
			this._attackTimer = 0; // 重置計時器

			// 2. 發射魔法彈
			this._launchMagicBullet();
		}
	}
}
  • 發出事件:當計時器到達 _attackInterval 時,我們呼叫 this.emit() 向外發出一個自定義事件。目的是為了讓子彈的運作與女巫本身解耦,簡單的說,就是 Witch 只負責「發起攻擊」,而不負責「子彈的生命週期」,這樣程式碼才不會過於複雜,讓程式碼職責更單一。
  • 發射點與方向:在 _launchMagicBullet() 中,我們計算了一個稍微偏離小女巫中心的發射點,讓子彈看起來是從掃帚末端發射出來的。

▸ 主循環整合與圖層管理

現在我們需要調整 start() 函數來連接 Witch 的事件與 MagicBullet 的實例化。

// ... (import 不變)

async function start() {

	// 將專案資源載入至遊戲中,並等待載入完成。(記得將資源別名修改成你自己專案的資源別名喔!)
	await pixi.assets
		// ... (其他素材不變)
		.add("ironman2025_cook.圖集動畫.特效") // 添加特效資源
		.load();

	// ... (初始化不變)

	// 特效圖層(用於加入各種場景特效,如魔法彈、敵人技能等)
	const effectLayer = new PIXI.Container();
	pixi.root.addChild(effectLayer);

	// ... (witch 創建不變)
    
	// 在外部接收小女巫攻擊事件,生成魔法彈並加入特效圖層
	witch.on(Witch.EVENT.ATTACK, (data: IMagicBulletData) => {
		const bullet = new MagicBullet(data);
		effectLayer.addChild(bullet);
	}, this);

	// 設置更新循環函數,每一幀都呼叫背景的 update 函式
	CG.Base2.addUpdateFunction((dt: number) => {
		// ... (其他 update() 不變)

		// 歷遍 effectLayer 的所有子物件去執行 update()
		const children = effectLayer.children;
		for (let i = children.length - 1; i >= 0; --i) {
			const effect = children[i];
			(effect as MagicBullet).update(dt);
		}
	});
}
  • effectLayer:我們創建了一個獨立的 PIXI.Container 作為「特效圖層」,用於將所有的特效整合在同一個圖層上,也方便管理。
  • 倒序迴圈:在更新子彈時,我們使用 for (let i = children.length - 1; i >= 0; --i) 倒序迴圈。這是因為當子彈飛出畫面時,它會呼叫 destroy() 將自己銷毀,並從 effectLayer.children 陣列中移除。如果我們使用正序迴圈,移除元素會導致陣列長度改變和後續元素的索引錯亂,而倒序迴圈就不會有這個問題。

小女巫發射魔法彈 預覽

點我查看範例程式碼

▸ 總結

我們今天成功為小女巫加入了自動攻擊能力,並透過 Tween 讓發射的魔法彈多了一點視覺上的小驚喜。

現在我們有了:

  • 移動:小女巫可以在畫面上自由飛行。
  • 攻擊:小女巫可以規律地發射魔法彈。
  • 基礎架構:將邏輯分裝到 WitchMagicBullet 兩個獨立類別中。

然而,有一個非常嚴重的問題開始慢慢浮出水面了。我們的 start() 函數開始變得越來越肥,越來越種了。它不僅要負責初始化所有物件,還要管理各種圖層(背景、特效)以及子彈的更新迴圈。當我們開始加入敵人和碰撞邏輯時,start() 將會不堪重負!

所以明天!我打算先來實作一個 Game 類別。這個類別將肩負起整合所有物件、管理圖層、處理遊戲流程的重責大任,讓我們的專案架構瞬間變得清晰!


上一篇
Day 17:玩家操控 - 讓小女巫自由飛行!
下一篇
Day 19:專案架構升級!實作 Game 類別,告別混亂的 start()
系列文
用 PixiJS 寫遊戲!告別繁瑣設定,在 Code.Gamelet 打造你的第一個遊戲23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言