iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
Rust

Bevy Rogue-lite 勇者冒險篇 × Rust 遊戲開發筆記系列 第 27

背景音樂與環境音系統

  • 分享至 

  • xImage
  •  

這篇的目標要把 bgm、巫師 Boss 的魔法球音效,以及選單點擊音全部整理進事件驅動的 Audio 系統,並且讓音量會隨 GamePhase 自動切換。主要會說明音效資產如何落到 ECS 架構裡,包含常數、資源、系統與插件串接的細節。

這回完成的重點:

  • 新增 assets/sounds/bgm/Space-Cadet.oggenemy/explosion3.oggui/click.ogg,集中載入並掛到 SoundEffects 資源。
  • 建立 MainThemeMusic 標記與 BackgroundMusicState,在 GamePhase 之間自動調整 bgm 音量:主選單 0.8、戰鬥 0.45、暫停完全靜音。
  • 將選單互動封裝成 MenuClickEvent,UI 音效透過事件轉給 Audio 系統播放。
  • 巫師 Boss 發射魔法球會發出 BossWizardSpellCastEvent,同樣走事件讓音效模組觸發。

音效資產與資源整併

SoundEffects 原本只存玩家攻擊/撿拾等音效,會擴充成標準的資產容器 (new_demo/src/resources/sound_effects.rs:5):

#[derive(Resource)]
pub struct SoundEffects {
    pub player_attack: Handle<AudioSource>,
    pub player_pickup: Handle<AudioSource>,
    pub player_upgrade: Handle<AudioSource>,
    pub player_hurt: Handle<AudioSource>,
    pub enemy_attack: Handle<AudioSource>,
    pub door_open: Handle<AudioSource>,
    pub door_close: Handle<AudioSource>,
    pub boss_wizard_spell: Handle<AudioSource>,
    pub ui_click: Handle<AudioSource>,
}

#[derive(Resource, Default)]
pub struct BackgroundMusicState {
    pub current_phase: Option<GamePhase>,
}

bgm 的音量數值集中在常數檔 (new_demo/src/constants.rs:43):

pub const MENU_MUSIC_VOLUME: f32 = 0.8;
pub const GAMEPLAY_MUSIC_VOLUME: f32 = 0.45;

要調整音量或換曲目時,只要修改常數或音效資源即可,避免在系統裡散落魔法數字。


背景音樂隨 GamePhase 自動切換

initialize_audionew_demo/src/systems/audio.rs:11)會在 Startup 階段一次載入所有音效,包含 bgm 的音樂檔案 Space-Cadet.ogg。它也會 spawn 一個帶有 MainThemeMusic 標記的實體來管理背景音樂:

commands.insert_resource(SoundEffects { /* handles... */ });
commands.insert_resource(BackgroundMusicState {
    current_phase: Some(session.phase()),
});

let mut settings = PlaybackSettings::LOOP.with_volume(Volume::Linear(match session.phase() {
    GamePhase::MainMenu => MENU_MUSIC_VOLUME,
    GamePhase::Playing | GamePhase::Paused => GAMEPLAY_MUSIC_VOLUME,
}));
if matches!(session.phase(), GamePhase::Paused) {
    settings.paused = true;
}

commands.spawn((MainThemeMusic, AudioPlayer::new(background_music), settings));

真正的「音量調整」由 update_background_music_volume 負責 (new_demo/src/systems/audio.rs:154)。它會監聽 GameSession 變化,並透過 AudioSink 對同一個播放實體調整音量或暫停:

match session.phase() {
    GamePhase::MainMenu => {
        sink.unmute();
        sink.set_volume(Volume::Linear(MENU_MUSIC_VOLUME));
        if sink.is_paused() {
            sink.play();
        }
    }
    GamePhase::Playing => {
        sink.unmute();
        sink.set_volume(Volume::Linear(GAMEPLAY_MUSIC_VOLUME));
        if sink.is_paused() {
            sink.play();
        }
    }
    GamePhase::Paused => {
        sink.set_volume(Volume::Linear(GAMEPLAY_MUSIC_VOLUME));
        if !sink.is_paused() {
            sink.pause();
        }
    }
}

BackgroundMusicState::current_phase 紀錄上一次的狀態,避免每幀都重複設定音量。這樣 bgm 進入主選單會恢復正常音量、遊戲中降低、暫停時完全停播,整體效果比單純停掉音效實體來得平順。


MenuClickEvent 統一 UI 音效入口

主選單與暫停選單的 Button 在被按下時會廣播 MenuClickEvent (new_demo/src/systems/game_session.rs:38):

Interaction::Pressed => {
    click_events.write(MenuClickEvent);
    match button.action {
        MainMenuAction::NewGame => start_events.write(StartNewGameEvent),
        MainMenuAction::LoadGame => load_events.write(RequestLoadGameEvent { from_main_menu }),
    }
}

AudioPluginplay_menu_click_sound 掛在 Update 排程 (new_demo/src/plugins/audio.rs:11)。系統只要看到事件就播放 sounds/ui/click.ogg (new_demo/src/systems/audio.rs:134):

if events.read().next().is_some() {
    spawn_one_shot(&mut commands, &sounds.ui_click);
}

好處是 UI 系統與音效完全解耦:未來新增別的選單或設定頁,也只要同樣寫入 MenuClickEvent 就能自動取得音效回饋。


巫師 Boss 魔法球音效

敵人模組新增 BossWizardSpellCastEvent (new_demo/src/systems/enemy.rs:20)。boss_wizard_ai_system 在施放魔法球時會同步寫入事件 (new_demo/src/systems/enemy.rs:483):

if distance <= WIZARD_BOSS_ATTACK_RADIUS && attack.cooldown.finished() {
    spawn_wizard_projectile(
        &mut commands,
        transform.translation,
        direction,
        attack_stat.value(),
    );
    spell_events.write(BossWizardSpellCastEvent);
    attack.cooldown.reset();
}

EnemyPlugin 負責註冊事件 (new_demo/src/plugins/enemy.rs:10),音效系統透過 play_boss_wizard_spell_sound 播放 enemy/explosion3.ogg (new_demo/src/systems/audio.rs:144)。因為事件只會在真正施法時觸發,避免了冷卻中還會重複播放的問題。


小結

目前有關新增的聲音部分已經完整納入事件流程,任何互動想要導入音效,只要寫入事件並在 Audio 模組註冊對應的 handler 就好。之後如果要新增關卡音樂或戰鬥 transition,可以沿用同一套 MainThemeMusic 標記與 BackgroundMusicState 的模式快速擴充。

因為這篇跟之前音效的部分比較難呈現出來,所以如果想實際體驗的話,建議可以到下方專案連結實際運作。

今日程式碼同步至 repo


上一篇
UI 介面統整與圖像化
下一篇
打擊特效與動畫調整
系列文
Bevy Rogue-lite 勇者冒險篇 × Rust 遊戲開發筆記28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言