iT邦幫忙

2025 iThome 鐵人賽

DAY 24
1
Modern Web

用 PixiJS 寫遊戲!告別繁瑣設定,在 Code.Gamelet 打造你的第一個遊戲系列 第 24

Day 24:簡易升級系統與技能選擇(二) - 升級選項與邏輯實作

  • 分享至 

  • xImage
  •  

昨天我們建立了升級介面的基礎框架,今天我們要來實作升級系統的核心邏輯部分。

首先,從今天開始我就不再重複過多舊有的程式碼了,只稍微修改的那種,我也改成統一在文章最後簡單匯總即可,因為我發現昨天的文章被這些內容拉的太長了,感覺會有點影響閱讀。因此接下來我就重點介紹跟今天的主題有關的內容,對其他優化有興趣的人在文章最後都會有專案的源碼網址可以去看看喔!

UpgradeOption:抽象的升級選項類別

// Games/Upgrades/UpgradeOption.ts
import { Game } from "../Game";

// 純資料類別 - 定義升級選項的內容
export abstract class UpgradeOption {

    /**
     * @param code - 升級選項的唯一代碼
     */
    constructor(readonly code: string) {}
    
    abstract get title(): string;
    abstract get description(): string;
    abstract get iconFrame(): string;
    abstract get weight(): number;
    
    /**
     * 應用此升級選項的效果
     * @param game - 遊戲實例
     */
    abstract apply(game: Game): void;
    
    /**
     * 是否可以選擇此選項(例如:前置條件檢查)
     * @param game - 遊戲實例
     */
    canSelect(game: Game): boolean {
        return true;
    }
}
  • abstract:這是一個抽象類別,代表它不能直接被實例化,必須被其他類別繼承後才能使用。這樣的設計可以確保所有升級選項都遵循相同的介面規範。
  • apply():玩家選擇選項後,實際會執行的程式碼將會由子類別在這個函數裡實作,例如提升最大生命值、加快移動速度、增加攻擊力等。
  • canSelect():用於在隨機出現選項時,決定該選項是否有機會出現。例如當玩家的武器等級滿了,升級武器的選項就不會出現之類的。

▸ 具體升級選項實作

有了 UpgradeOption 這個抽象類別之後,我們就可以開始繼承它去實作各式各樣不同的升級選項了!

這邊我就舉幾個比較不一樣的例子。

// Games/Upgrades/Attributes/IncreaseHealth.upgrade.ts
import { Game } from "src/Games/Game";
import { UpgradeOption } from "../UpgradeOption";

// 生命提升升級選項
export class IncreaseHealthUpgradeOption extends UpgradeOption {

    constructor() {
        super("increase_health");
    }

    get title(): string { return "生命強化" }
    get description(): string { return "最大生命值 +3" }
    get iconFrame(): string { return "icon_health_up" }
    get weight(): number { return 1 }

    /**
     * 應用此升級選項的效果
     * @param game - 遊戲實例
     */
    apply(game: Game): void {
        const witch = game.witch;
        witch.setHp(witch.hp, witch.maxHp + 3);
    }
}

這應該是最單純的升級選項,選擇後會增加小女巫的最大生命值。

// Games/Upgrades/Attributes/IncreaseExpGain.upgrade.ts
import { Game } from "src/Games/Game";
import { UpgradeOption } from "../UpgradeOption";

// 經驗值獲取提升升級選項
export class IncreaseExpGainUpgradeOption extends UpgradeOption {

    constructor() {
        super("increase_exp_gain");
    }

    get title(): string { return "智慧提升" }
    get description(): string { return "經驗值獲取 +25%" }
    get iconFrame(): string { return "icon_exp_up" }
    get weight(): number { return 0.8 } // 稍微降低權重,因為這是個很有用的升級

    /**
     * 應用此升級選項的效果
     * @param game - 遊戲實例
     */
    apply(game: Game): void {
        // 提升經驗值獲取 25%
        game.multiplyExpGain(1.25);
    }
}

這也滿單純的,但稍微降低了點權重,等等實作升級管理器的時候,就會根據這些選項的權重來隨機抽取要讓玩家選擇的選項。

// Games/Upgrades/Attributes/IncreaseAttackSpeed.upgrade.ts
import { Game } from "src/Games/Game";
import { UpgradeOption } from "../UpgradeOption";

// 攻擊速度提升升級選項
export class IncreaseAttackSpeedUpgradeOption extends UpgradeOption {

    constructor() {
        super("increase_attack_speed");
    }

    get title(): string { return "急速施法" }
    get description(): string { return "攻擊間隔 -100ms" }
    get iconFrame(): string { return "icon_magic_bullet" }
    get weight(): number { return 1 }

    /**
     * 應用此升級選項的效果
     * @param game - 遊戲實例
     */
    apply(game: Game): void {
        const witch = game.witch;
        // 減少攻擊間隔時間,但不少於 100ms
        const newInterval = witch.attackInterval - 100;
        witch.setAttackInterval(newInterval);
    }

    /**
     * 檢查是否可以選擇此選項
     * @param game - 遊戲實例
     */
    canSelect(game: Game): boolean {
        // 只在攻擊間隔還能降低時才能選擇
        return game.witch.attackInterval > 100;
    }
}

這邊就多了一個 canSelect 的判定,畢竟不能讓玩家的武器攻擊間隔一直減少到 0,甚至是負數嘛!

UpgradeManager:升級管理器

為了統一管理所有升級選項,我們要建立一個升級管理器來負責這項工作。這個管理器的核心是實作加權隨機(Weighted Random Selection),確保稀有升級的出現機率是可控的。

import { Game } from "../Game";
import { UpgradeOption } from "./UpgradeOption";
import { IncreaseHealthUpgradeOption } from "./Attributes/IncreaseHealth.upgrade";
import { IncreaseAttackUpgradeOption } from "./Attributes/IncreaseAttack.upgrade";
import { IncreaseSpeedUpgradeOption } from "./Attributes/IncreaseSpeed.upgrade";
// ... (其他升級引用省略)

export class UpgradeManager {

    // 所有可用的升級選項
    private _allUpgrades: UpgradeOption[] = [];
    
    // 已選擇的升級選項(用於追蹤玩家已獲得的升級)
    private _selectedUpgrades: Set<string> = new Set();

    constructor(private _game: Game) {
        // 註冊所有升級選項
        this._registerUpgrades();
    }

    /**
     * 註冊所有升級選項
     */
    private _registerUpgrades(): void {
        this._allUpgrades.push(
            // 屬性強化升級
            new IncreaseHealthUpgradeOption(),
            new IncreaseAttackUpgradeOption(),
            new IncreaseSpeedUpgradeOption(),
            // ... (其他升級省略)
            // 這裡可以繼續添加其他升級選項(技能類等)
        );
    }

    /**
     * 隨機選擇指定數量的升級選項
     * @param count - 要選擇的數量
     */
    getRandomUpgrades(count: number): UpgradeOption[] {
        // 過濾出可選的升級選項
        const availableUpgrades = this._allUpgrades.filter(upgrade => 
            upgrade.canSelect(this._game)
        );

        if (availableUpgrades.length === 0) {
            return [];
        }

        // 根據權重進行加權隨機選擇
        const selected: UpgradeOption[] = [];
        const tempUpgrades = [...availableUpgrades];

        for (let i = 0; i < count && tempUpgrades.length > 0; i++) {
            const totalWeight = tempUpgrades.reduce((sum, upgrade) => sum + upgrade.weight, 0);
            let random = Math.random() * totalWeight;
            
            let selectedIndex = 0;
            for (let j = 0; j < tempUpgrades.length; j++) {
                random -= tempUpgrades[j].weight;
                if (random <= 0) {
                    selectedIndex = j;
                    break;
                }
            }

            selected.push(tempUpgrades[selectedIndex]);
            tempUpgrades.splice(selectedIndex, 1); // 避免重複選擇
        }

        return selected;
    }

    /**
     * 應用升級選項
     * @param upgrade - 要應用的升級選項
     */
    applyUpgrade(upgrade: UpgradeOption): void {
        upgrade.apply(this._game);
        this._selectedUpgrades.add(upgrade.code);
    }

    /**
     * 檢查是否已選擇某個升級
     * @param upgradeCode - 升級選項代碼
     */
    hasUpgrade(upgradeCode: string): boolean {
        return this._selectedUpgrades.has(upgradeCode);
    }

    /**
     * 取得已選擇的升級數量
     * @param upgradeCode - 升級選項代碼
     */
    getUpgradeCount(upgradeCode: string): number {
        // 如果需要支援同一升級多次選擇,可以改用 Map<string, number>
        return this._selectedUpgrades.has(upgradeCode) ? 1 : 0;
    }
}

加權隨機選擇邏輯

  • 首先過濾出所有可選的升級選項(通過 canSelect() 檢查)。
  • 計算所有選項的總權重。
  • 使用隨機數按權重比例選擇升級選項。
  • 確保不會選擇重複的選項。

這樣,我們就可以讓一些比較強力的升級選項(如經驗值提升)出現機率較低,保持遊戲平衡。

▸ 更新 LevelUpLayer:介面整合

更新昨天 LevelUpLayer 留白的地方來使用新的升級系統。

// Games/Uis/LevelUpLayer.ts

/**
 * 當小女巫升級時。
 */
private _onWitchLevelUp(): void {

    this._game.pause();  // 暫停遊戲
    this.visible = true; // 顯示介面

    // 使用升級管理器獲取隨機升級選項
    const upgrades = this._game.upgradeManager.getRandomUpgrades(3);

    for (let i = 0; i < this._choices.length && i < upgrades.length; ++i) {
        const choice = this._choices[i];
        const icon = choice["icon"] as PIXI.Sprite;
        const titleText = choice["titleText"] as PIXI.Text;
        const descText = choice["descText"] as PIXI.Text;
        const upgrade = upgrades[i];

        // 更新圖示
        const texture = pixi.assets.getSpritesheet("LittleWitch_TheJourney.圖集動畫.uis").textures[upgrade.iconFrame];
        icon.texture = texture;

        // 更新文字內容
        titleText.text = upgrade.title;
        descText.text = upgrade.description;

        choice["upgrade"] = upgrade; // 儲存升級選項參考
    }
}

/**
 * 當選項被點擊時
 */
private _onChoiceTap(e: PIXI.FederatedPointerEvent): void {

    // 獲取被點擊的選項
    const choice = e.target as PIXI.Container;
    const upgrade = choice["upgrade"];

    // 應用升級效果
    upgrade && this._game.upgradeManager.applyUpgrade(upgrade);
    
    this.visible = false; // 隱藏介面
    this._game.resume();  // 繼續遊戲
}
  • icon.texture:我們在創建 Sprite 時會帶入 texture 這個參數來決定顯示的紋理,而我們也能夠隨時將這個 texture 替換掉,就可以套用新的紋理了。

▸ 其他優化(程式碼結構與封裝性改善)

今天除了升級系統的核心功能外,還進行了一些優化:

  • 屬性封裝改善:為 WitchGame 類別添加了專門的屬性設定方法(如 multiplyMoveSpeed()setAttackInterval() 等),避免直接修改私有屬性。
  • 升級介面文字:在選項 UI 中添加了標題和描述文字,讓玩家更清楚了解每個升級選項的效果。以及標題的「選擇一個增益!」的提示文字。
  • 經驗值倍率系統:在 Game 類別中加入經驗值倍率機制,讓經驗值相關的升級能正確作用。
  • 血量改變事件:由於小女巫讓生命提升的升級選項,需要讓選擇選項後同步更新血條的顯示,因此 BaseCharacter 添加了新的 HP_CHANGED 事件,並調整 HealthBar 使其可以監聽血量變化更新顯示。這樣也可以將受傷的監聽移除了,因為受傷改變血量也一定會發送 HP_CHANGED 事件。

小女巫 升級系統 預覽

點我查看 Day 24 範例程式碼點我查看最新進度程式碼

▸ 總結

我們今天成功將升級系統給打造完成了!並且優化了一些程式架構,我們來回顧一下今天所做的內容:

  • 升級選項抽象類別:統一的升級選項介面,使新增升級變得容易且結構化。
  • 多種屬性升級選項:實作了生命、攻擊、速度、攻速、經驗值等具體的能力提升邏輯。
  • 升級管理器:負責選項註冊、加權隨機選擇、canSelect 條件過濾與效果應用,掌握了 Roguelike 遊戲的核心成長體驗。
  • 介面整合:升級選項的實例與 LevelUpLayer UI 上的顯示與點擊事件完全串接。

感覺把一些跟主題無關的調整、優化從文章省略後,整體的可讀性變高了不少,也讓我在優化一些小地方的時候,不用再考慮需要一一在文章中展示出來,會佔了整個文章的篇幅,真的是應該早點發現的。

明天,我們來做點比較簡單的東西,實作敵人死亡後掉落道具的機制,並加入角色受傷閃白等視覺回饋,讓魔法彈有真的打到敵人的感覺,使戰鬥體驗更加刺激!


上一篇
Day 23:簡易升級系統與技能選擇(一) - 遊戲暫停與升級介面架構
系列文
用 PixiJS 寫遊戲!告別繁瑣設定,在 Code.Gamelet 打造你的第一個遊戲24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言