目前的遊戲缺少聲音還是少了點臨場感,玩遊戲的音效還是蠻重要的,當你操作角色做出一些行為,例如攻擊敵人時,會有攻擊的音效,才會有點回饋感。
而 Kenney 所提供的資源也很多,連音效都有提供,可以去實際每一個試聽一下。

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

今天設定的重點:
AudioPlugin,監聽事件後以一次性 AudioPlayer 播放音效。這次先新增 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,不再重複查字串或重新載入。
這次的重構重點在於事件。每個會觸發音效的行為,都對應一個事件型別:
PlayerMeleeAttackEvent,音效系統只要監聽即可。PlayerPickupEvent,地上撿拾與寶箱獲得道具都發這個事件。(new_demo/src/systems/items.rs, new_demo/src/systems/chest.rs)PlayerPoisonDamageEvent 確保毒傷 tick 時會再送一個事件給音效層。(new_demo/src/systems/player_status.rs)EnemyAttackHitEvent 在 enemy_contact_attack_system 確認扣血後送出。(new_demo/src/systems/enemy.rs)DoorStateChangedEvent 讓音效系統知道門目前是開還是關,好切換對應檔案。(new_demo/src/systems/door_interaction.rs)PlayerLevelUpEvent,升級時播 fanfare。對應的 plugin 也各自 add_event,把事件註冊進 App(例如 PlayerPlugin 連帶註冊 PlayerPoisonDamageEvent)。這樣音效層只要注意事件,就能掌握遊戲當下的互動狀態。
音效播放統一放進 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 會自行決定幾時開始播、幾時收尾。這種做法很適合短音效,不需要額外管控播放狀態。
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 的範例了,但是如果有興趣的話,可以到下方提供的專案連結到今天的進度,去執行後就可以實際操作,就可以體驗到:
音效架構改成事件導向後,要再增加 UI 點擊或音效就很簡單,只要新增事件與播放系統就好。
今日程式碼同步至 repo