iT邦幫忙

2025 iThome 鐵人賽

DAY 20
1
Rust

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

事件驅動的音效系統

  • 分享至 

  • xImage
  •  

目前的遊戲缺少聲音還是少了點臨場感,玩遊戲的音效還是蠻重要的,當你操作角色做出一些行為,例如攻擊敵人時,會有攻擊的音效,才會有點回饋感。

Kenney 所提供的資源也很多,連音效都有提供,可以去實際每一個試聽一下。

音效素材

我挑了幾個音效檔案放在 assets/sounds/ 中,目前先實作有關玩家跟敵人還有音效(Sound Effects)。把這些整理好的素材正式接到遊戲裡,讓戰鬥、撿寶、升級甚至開門都有對應音效。

音效分類

今天設定的重點:

  • 建立一個統一的音效資源,集中管理常用的 Handle 與載入流程。
  • 把攻擊、撿拾、中毒扣血、門的互動拆成事件,避免在各系統硬塞播放邏輯。
  • 寫一個專用的 AudioPlugin,監聽事件後以一次性 AudioPlayer 播放音效。

SoundEffects 資源與 Startup 載入流程

這次先新增 new_demo/src/resources/sound_effects.rs,內容是一個單純的 SoundEffects 資源。把玩家攻擊、撿拾、升級、持續受傷,以及敵人攻擊、門開門/關門等音效 Handle 通通集中在一起:

#[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>,
}

搭配的 load_sound_effects 寫在 new_demo/src/systems/audio.rs,它會在 Startup 階段依序呼叫 asset_server.load() 把檔案讀進來,最後插入 SoundEffects 資源。後面所有播放邏輯只需要 clone Handle,不再重複查字串或重新載入。


事件驅動讓音效與邏輯分離

這次的重構重點在於事件。每個會觸發音效的行為,都對應一個事件型別:

  1. 玩家攻擊:原本就有 PlayerMeleeAttackEvent,音效系統只要監聽即可。
  2. 玩家撿拾道具:新增 PlayerPickupEvent,地上撿拾與寶箱獲得道具都發這個事件。(new_demo/src/systems/items.rs, new_demo/src/systems/chest.rs
  3. 中毒扣血:新的 PlayerPoisonDamageEvent 確保毒傷 tick 時會再送一個事件給音效層。(new_demo/src/systems/player_status.rs
  4. 敵人攻擊命中EnemyAttackHitEventenemy_contact_attack_system 確認扣血後送出。(new_demo/src/systems/enemy.rs
  5. 門的開關DoorStateChangedEvent 讓音效系統知道門目前是開還是關,好切換對應檔案。(new_demo/src/systems/door_interaction.rs
  6. 玩家升級:沿用 PlayerLevelUpEvent,升級時播 fanfare。

對應的 plugin 也各自 add_event,把事件註冊進 App(例如 PlayerPlugin 連帶註冊 PlayerPoisonDamageEvent)。這樣音效層只要注意事件,就能掌握遊戲當下的互動狀態。


AudioPlugin 一次性實體播放音效

音效播放統一放進 new_demo/src/plugins/audio.rs

impl Plugin for AudioPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(Startup, load_sound_effects).add_systems(
            Update,
            (
                play_player_attack_sound,
                play_enemy_attack_sound,
                play_player_pickup_sound,
                play_player_level_up_sound,
                play_player_poison_damage_sound,
                play_door_state_sound,
            ),
        );
    }
}

play_... 系統們都遵循相同做法:

  • EventReader 把排隊的事件掃過一遍,只要收到至少一筆,就觸發音效。
  • 使用 spawn_one_shot(commands, handle),生成一個含 AudioPlayer 的 entity。
  • PlaybackSettings::DESPAWN 會在音效播完後自動刪除該 entity,避免留下垃圾。

另外補了一點細節是 spawn_one_shot 收到的是 Handle<AudioSource>,只要 clone 一份就能交給 AudioPlayer::new,Bevy 會自行決定幾時開始播、幾時收尾。這種做法很適合短音效,不需要額外管控播放狀態。


main.rs 插入音效插件

AudioPlugin 最後被安插在 main.rs 的 plugin 列表中,在 WorldPlugin 後、PlayerPlugin 前:

App::new()
    .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
    .add_plugins((
        WorldPlugin,
        AudioPlugin,
        PlayerPlugin,
        UiPlugin,
        EnemyPlugin,
        ProgressionPlugin,
        ItemPlugin,
        ChestPlugin,
        EquipmentPlugin,
        CameraPlugin,
        AttackPlugin,
        WallCollisionPlugin,
        DoorInteractionPlugin,
        RoomTransitionPlugin,
    ))
    .run();

這樣可以確保音效資源在玩家與敵人系統執行前就準備到位。PlayerPlugin 等也註冊了各自的事件,因此整個音效流程算是平行在主要邏輯旁邊運作。


小結

因為要錄下目前遊戲的聲音有點麻煩,所以這篇文章就沒有 demo 的範例了,但是如果有興趣的話,可以到下方提供的專案連結到今天的進度,去執行後就可以實際操作,就可以體驗到:

  • 空白鍵揮刀會打出攻擊聲。
  • 撿道具或開寶箱都會有撿起的聲音,有比較好的回饋。
  • 升級的話會放出升級的音效,搭配 HUD 文字讓成長感更強。
  • 中毒扣血時會持續提醒,玩家不會忽略身上還有毒。
  • 史萊姆命中玩家、門的開關同樣有音效。

音效架構改成事件導向後,要再增加 UI 點擊或音效就很簡單,只要新增事件與播放系統就好。

今日程式碼同步至 repo


上一篇
角色成長與經驗系統
下一篇
攻擊瞄準框與方向鎖定
系列文
Bevy Rogue-lite 勇者冒險篇 × Rust 遊戲開發筆記24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言