經過昨天的升級系統實作,我們的小女巫終於能夠在戰鬥中成長了!今天,我們要來做點比較簡單的東西:
在開始程式碼實作之前,讓我們先來思考一下道具系統的設計架構。
為了實現這些功能,我打算建立了三個核心類別:
ItemType
:定義道具的基本屬性(名稱、效果、外觀等)。就像 EnemyTye
一樣。BaseItem
:單個道具實例的行為邏輯。就像 Enemy
一樣。ItemManager
:統一管理道具的生成與掉落。ItemType
:道具類型定義首先讓我們看看道具類型的定義。每種道具都有自己的配置,包含圖案、拾取範圍、掉落權重和使用效果:
// Games/Items/ItemType.ts
import { Game } from "../Game";
export interface IItemConfig {
code?: string;
name: string;
description: string;
iconFrame: string;
// 吸取範圍:物品會開始自動飛向小女巫的距離(像素)
pickUpRange?: number;
// 掉落權重:數值越高,掉落機率越大
dropWeight?: number;
useEffect: (game: Game) => void;
}
export class ItemType {
static readonly ALL: { [code: string]: ItemType } = {};
static HealthPotion = new ItemType("health_potion", {
name: "恢復藥水",
description: "恢復生命值 20%",
iconFrame: "health_potion",
pickUpRange: 0, // 需要直接接觸才能拾取
dropWeight: 1.0,
useEffect: (game: Game) => {
const witch = game.witch;
const healAmount = Math.ceil(witch.maxHp * 0.2);
witch.setHp(witch.hp + healAmount, witch.maxHp);
}
});
static ExpOrb = new ItemType("exp_orb", {
name: "經驗球",
description: "獲得經驗值",
iconFrame: "exp_gems",
pickUpRange: 250, // 較大的吸取範圍
dropWeight: 3.0, // 較高的掉落權重
useEffect: (game: Game) => {
const witch = game.witch;
witch.gainExp(10); // 給予 10 點經驗值
}
});
constructor(
readonly code: string,
readonly config: IItemConfig
) {
config.code = code;
ItemType.ALL[code] = this;
}
}
這裡我們定義了兩種基本道具:
BaseItem
:道具行為邏輯接下來是單個道具的行為實作。這個類別代表道具在舞台畫面上的顯示物件,因此每個道具都會繼承 GameObject
,具備碰撞檢測和更新循環:
// Games/Items/BaseItem.ts
import { Game } from "../Game";
import { GameObject } from "../GameObject";
import pixi = CG.Pixi.pixi;
import { ItemType } from "./ItemType";
export class BaseItem extends GameObject {
constructor(game: Game, private _type: ItemType) {
super(game);
this.setSpriteFrame(_type.config.iconFrame);
this.setHitBox(new PIXI.Circle(0, 0, 16));
}
// 物品道具類型
get type(): ItemType { return this._type }
/**
* 設置物品道具的圖幀。
* @param frameName - 物品道具圖幀名稱(參考圖集動畫資源的圖幀列表)
*/
setSpriteFrame(frameName: string): void {
const spritesheet = pixi.assets.getSpritesheet("LittleWitch_TheJourney.圖集動畫.物品道具");
const texture = spritesheet.textures[frameName];
this.setSpriteTexture(texture);
}
/**
* 更新循環函數。
* @param dt - 每幀間隔時間(ms)
*/
update(dt: number): void {
const { pickUpRange, useEffect } = this._type.config;
const witch = this.game.witch;
// 道具跟著背景往左移動
this.x -= 0.15 * dt;
// 檢查是否移出畫面左側,如果是則自動銷毀
if (this.x < -50) {
this.destroy({ children: true });
return;
}
if(typeof pickUpRange === "number") {
const dx = witch.x - this.x;
const dy = witch.y - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// 如果在吸取範圍內,開始向小女巫移動
if(distance < pickUpRange) {
// 距離越近,移動速度越快
const speed = 0.9 * (1 - (distance / pickUpRange));
this.x += dx / distance * speed * dt;
this.y += dy / distance * speed * dt;
}
}
// 如果碰到小女巫,觸發效果並銷毀物品
if(this.hitTest(witch)) {
useEffect(this.game);
this.destroy({ children: true });
}
}
}
hitTest()
來檢測。useEffect
。由於
"LittleWitch_TheJourney.圖集動畫.物品道具"
是新的資源素材,app.ts
的start()
函數裡面也要添加對應的載入程式碼。
ItemManager
:道具掉落管理最後是道具管理器,負責統一處理道具的生成和掉落邏輯:
// Games/Items/ItemManager.ts
import { Game } from "../Game";
import { BaseItem } from "./BaseItem";
import { ItemType } from "./ItemType";
export class ItemManager {
// 所有可掉落的道具類型
private _droppableItems: ItemType[] = [];
constructor(private _game: Game) {
this._registerDroppableItems();
}
/**
* 註冊所有可掉落的道具類型
*/
private _registerDroppableItems(): void {
this._droppableItems.push(
ItemType.HealthPotion,
ItemType.ExpOrb
);
}
/**
* 在指定位置掉落隨機道具
* @param x - X 座標
* @param y - Y 座標
* @param dropChance - 基礎掉落機率 (0-1)
*/
dropRandomItem(x: number, y: number, dropChance: number = 0.1): void {
if (Math.random() > dropChance) return;
// 根據權重選擇道具類型
const totalWeight = this._droppableItems.reduce((sum, type) =>
sum + (type.config.dropWeight || 1), 0);
let random = Math.random() * totalWeight;
let selectedType: ItemType | null = null;
for (const type of this._droppableItems) {
random -= (type.config.dropWeight || 1);
if (random <= 0) {
selectedType = type;
break;
}
}
if (selectedType) {
this._createItem(selectedType, x, y);
}
}
/**
* 創建道具實例並添加到場景中
* @param itemType - 道具類型
* @param x - X 座標
* @param y - Y 座標
*/
private _createItem(itemType: ItemType, x: number, y: number): void {
const item = new BaseItem(this._game, itemType);
item.position.set(x, y);
// 添加到遊戲的效果圖層中
this._game.effectLayer.addChild(item);
}
}
這個管理器實作了加權隨機掉落系統,讓不同道具有不同的掉落機率。
現在我們需要將道具掉落整合到敵人的死亡流程中。在 Game
類別中加入道具管理器:
// ... (其餘 import 省略)
import { ItemManager } from './Items/ItemManager';
export class Game extends PIXI.Container {
// ... (其餘省略)
private _itemManager: ItemManager;
constructor() {
super();
// ... (其餘省略)
this._itemManager = new ItemManager(this);
}
// 升級管理器
get itemManager(): ItemManager { return this._itemManager }
}
然後在敵人的死亡處理中加入道具掉落:
// Games/Characters/Enemys/BaseEnemy.ts
/**
* 讓敵人死亡並掉落道具。
* @override 覆寫 BaseCharacter 的死亡函數,添加道具掉落
*/
async death(): Promise<void> {
if (!this.isAlive) return;
// 在死亡位置掉落道具
this.game.itemManager.dropRandomItem(this.x, this.y, 0.4);
// 呼叫基底類別的死亡處理
return super.death();
}
ColorOverlayFilter
的應用接下來我們要來實作受傷閃白效果。PixiJS 提供了豐富的濾鏡系統,其中 ColorOverlayFilter
正好適合我們的需求,它用於將一個純色疊加在圖像上面。
PixiJS 的濾鏡是一個額外的套件,如果是在自己的環境運行,需自行安裝 pixijs/filters 擴充套件。
我打算直接加在 BaseCharacter
裡面,讓所有角色受傷都會有閃白效果。
// ... (其餘省略)
export class BaseCharacter extends GameObject {
// ... (其餘省略)
// 受傷閃白濾鏡
private _flashFilter: PIXI.filters.ColorOverlayFilter;
constructor(game: Game) {
super(game);
// ... (其餘省略)
// 初始化白色疊加濾鏡
const flashFilter = this._flashFilter = new PIXI.filters.ColorOverlayFilter({
color: 0xFFFFFF // 白色
});
// 一開始讓濾鏡不啟用
flashFilter.enabled = false;
this.filters = [flashFilter];
}
/**
* 受到傷害。
* @param damage - 傷害數值
*/
takeDamage(damage: number): void {
this.setHp(this._hp - damage);
this.emit(BaseCharacter.EVENT.HURT, { damage: damage });
// 如果受到傷害
if(damage > 0) {
// 且現在沒有啟用濾鏡
if(!this._flashFilter.enabled) {
// 啟用濾鏡,並等待 50 毫秒後重設
this._flashFilter.enabled = true;
this.game.wait(50).then(() => {
this._flashFilter.enabled = false;
});
}
}
}
}
除了 ColorOverlayFilter
以外,pixijs/filters
還有非常非常多好用的濾鏡,像是陰影、發光、RGB 分離、模糊、老電影濾鏡等等,想要實際測試看看效果的人,可以到 PixiJS 官方示範的網站上去玩玩~
今天我們成功實作了道具掉落系統和受傷閃白特效。道具系統讓我們的遊戲更加往 Roguelike 的道路邁進了一步。而受傷閃白特效我個人覺得極大幅度提升了遊戲的回饋感,讓小女巫的魔法彈真的有把敵人幹掉的感覺,而不是輕飄飄地摸到一下就消失了。尤其是針對高血量的敵人,每次受傷的閃白都會讓玩家清楚知道攻擊有效果。
明天,我們要進入這個系列的最高潮了!我們要把最終大魔王放進這個世界,除了高血量,還會有多種攻擊模式。這不僅是對小女巫,對玩家的考驗,也是對我自己的考驗!