本來沒有想要做角色升級的機制,但看到圖片素材中角色有分成不同裝備,就覺得應該很有趣,可以來做做看。所以這篇就是要把專案中的角色成長系統搭建起來。
預設玩家角色會從 Lv0 開始,透過擊敗敵人累積經驗值,升級時自動更新基礎攻、防以及角色圖案,而 HUD 也能即時顯示等級進度。
預計達成的目標:

四個等級角色素材
新增的 new_demo/src/components/progression.rs 定義 PlayerProgression component,專門儲存玩家等級與累積經驗,並提供查詢基礎數值的 helper:
#[derive(Component, Debug, Clone)]
pub struct PlayerProgression {
    pub level: usize,
    pub experience: u32,
}
impl PlayerProgression {
    pub fn new() -> Self { Self { level: 0, experience: 0 } }
    pub fn next_level_requirement(&self) -> Option<u32> {
        PLAYER_LEVEL_XP_REQUIREMENTS.get(self.level).copied()
    }
    pub fn base_attack(&self) -> i32 {
        PLAYER_LEVEL_BASE_ATTACK
            .get(self.level)
            .copied()
            .unwrap_or_else(|| *PLAYER_LEVEL_BASE_ATTACK.last().unwrap())
    }
    pub fn sprite_path(&self) -> &'static str {
        PLAYER_LEVEL_SPRITE_PATHS
            .get(self.level)
            .copied()
            .unwrap_or_else(|| *PLAYER_LEVEL_SPRITE_PATHS.last().unwrap())
    }
}
相關常數集中在 new_demo/src/constants.rs:
pub const PLAYER_MAX_LEVEL: usize = 3;
pub const PLAYER_LEVEL_XP_REQUIREMENTS: [u32; PLAYER_MAX_LEVEL] = [120, 240, 420];
pub const PLAYER_LEVEL_BASE_ATTACK: [i32; PLAYER_MAX_LEVEL + 1] = [15, 24, 34, 46];
pub const PLAYER_LEVEL_BASE_DEFENSE: [i32; PLAYER_MAX_LEVEL + 1] = [4, 7, 11, 16];
pub const PLAYER_LEVEL_SPRITE_PATHS: [&str; PLAYER_MAX_LEVEL + 1] = [
    "characters/players/knight_lv0.png",
    "characters/players/knight_lv1.png",
    "characters/players/knight_lv2.png",
    "characters/players/knight_lv3.png",
];
這樣之後調整等級曲線時,只需要修改常數就可以同步影響所有系統。
new_demo/src/systems/setup.rs 把 progression 掛在玩家身上,一出場時就依照 Lv0 的設定初始化攻、防與角色圖案:
let progression = PlayerProgression::new();
commands
    .spawn((
        Player,
        Sprite::from_image(asset_server.load(progression.sprite_path())),
        Health::new(PLAYER_INITIAL_HEALTH),
        Attack::new(progression.base_attack()),
        Defense::new(progression.base_defense()),
        Stamina::new(PLAYER_MAX_STAMINA, PLAYER_STAMINA_REGEN_PER_SECOND),
        EquippedWeapon::new(WeaponKind::Level1),
        progression,
    ));

這代表角色會以 knight_lv0.png 亮相,之後升級時則由 progression 決定下一張圖。
在敵人淡出前的 despawn_dead_enemies_system 中,新增 EnemyDefeatedEvent 記錄來源與獲得的經驗值 (new_demo/src/systems/enemy.rs):
if experience_reward > 0 {
    defeated_events.write(EnemyDefeatedEvent {
        experience: experience_reward,
        enemy_name: enemy_label,
    });
    info!("擊敗{}獲得 {} EXP", enemy_label, experience_reward);
}
目前預設:史萊姆 30、獨眼巨人 90、寶箱怪 110。後續要調整節奏時,直接改 constants.rs 即可。
為集中成長邏輯,建立 ProgressionPlugin (new_demo/src/plugins/progression.rs) 並在 main.rs 內掛上:
App::new()
    .add_plugins((
        WorldPlugin,
        PlayerPlugin,
        UiPlugin,
        EnemyPlugin,
        ProgressionPlugin,
        // ...
    ))
插件註冊兩個核心系統 (new_demo/src/systems/progression.rs):
apply_enemy_experience_rewards:累加經驗值、處理可能跨越多級的升級,並發送 PlayerLevelUpEvent。apply_player_level_up_effects:收到升級事件後更新 Attack/Defense 基礎值與角色 sprite。attack.base = progression.base_attack();
defense.base = progression.base_defense();
sprite.image = asset_server.load(progression.sprite_path());
info!(
    "玩家等級提升至 Lv.{}!基礎攻擊 {},基礎防禦 {}",
    level, attack.base, defense.base,
);
系統排程在敵人淡出之後執行,確保擊倒敵人後 HUD 與 Console 立即反映最新狀態。如果玩家已達 Lv3,則會在 log 中提示經驗值不再累積。
為了讓成長資訊更直觀,update_player_stats_panel (new_demo/src/systems/player_stats.rs) 新增等級與經驗行,並調整面板高度:
let level_line = if let Some(requirement) = progression.next_level_requirement() {
    format!(
        "LV   {:>2}  EXP {:>4}/{:>4}",
        progression.level,
        progression.experience,
        requirement,
    )
} else {
    format!("LV   {:>2}  EXP MAX", progression.level)
};

畫面左上角除了原本的攻防/耐力資訊,現在還能看到等級進度條數字。配合 Console log,可以很清楚地確認升級節奏是否符合預期。
ProgressionPlugin 負責,後續擴充更容易。接下來考慮加上的功能:
今日程式碼同步至 repo