diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d29444..36edae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ through `EngineState:audio_manager`) - Custom fonts may now be set on a `TextActor` at creation time. `EngineState::add_text_actor_with_font` was added for a convenience. The font specified should be a `.ttf` or `.otf` file stored in `assets/fonts` +- Custom sounds may now be played via `AudioManager::play_music` and `AudioManager::play_sfx` by +specifying a path to a sound file relative to `assets/audio`. - (meta) Improved CI times by using sccache together with GitHub Actions caching diff --git a/assets/audio/README.md b/assets/audio/README.md new file mode 100644 index 0000000..4f0af6d --- /dev/null +++ b/assets/audio/README.md @@ -0,0 +1 @@ +You may add your own sound effects to your own local copy of this directory. Please see the `audio` module documentation for details. diff --git a/examples/music.rs b/examples/music.rs index 7f8d15f..8e981fc 100644 --- a/examples/music.rs +++ b/examples/music.rs @@ -1,44 +1,26 @@ -use rusty_engine::prelude::*; +//! This is an example of playing a music preset. For playing your own music file, please see the +//! `sound` example. -struct GameState { - music_index: usize, -} +use rusty_engine::prelude::*; -rusty_engine::init!(GameState); +rusty_engine::init!(); fn main() { let mut game = Game::new(); let msg = game.add_text_actor( "msg", - "Press any key to advance to the next music selection.\n\nIf you are not running with \"--release\", it may take several seconds for each song to load!", + "You can play looping music presets that are included in the asset pack. For example:", ); - msg.translation.y = -200.0; + msg.translation.y = 50.0; - game.add_logic(logic); - game.run(GameState { music_index: 0 }); -} + let msg2 = game.add_text_actor_with_font( + "msg2", + "engine_state.audio_manager.play_music(MusicPreset::Classy8Bit, 1.0);", + "FiraMono-Medium.ttf", + ); + msg2.translation.y = -50.0; -fn logic(engine_state: &mut EngineState, game_state: &mut GameState) -> bool { - let mut should_play_new_song = false; - // Play a new song because a key was pressed - for ev in engine_state.keyboard_events.drain(..) { - if ev.state != ElementState::Pressed { - continue; - } - game_state.music_index = (game_state.music_index + 1) % MusicPreset::variant_iter().count(); - should_play_new_song = true; - break; - } + game.audio_manager.play_music(MusicPreset::Classy8Bit, 1.0); - if should_play_new_song || !engine_state.audio_manager.music_playing() { - // Actually play the new song - let music_preset = MusicPreset::variant_iter() - .nth(game_state.music_index) - .unwrap(); - engine_state.audio_manager.play_music(music_preset, 1.0); - // And make a text actor saying what song we're playing - let note1 = engine_state.add_text_actor("note1", format!("Looping: {:?}", music_preset)); - note1.font_size = 75.0; - } - true + game.run(()); } diff --git a/examples/music_sampler.rs b/examples/music_sampler.rs new file mode 100644 index 0000000..7f8d15f --- /dev/null +++ b/examples/music_sampler.rs @@ -0,0 +1,44 @@ +use rusty_engine::prelude::*; + +struct GameState { + music_index: usize, +} + +rusty_engine::init!(GameState); + +fn main() { + let mut game = Game::new(); + let msg = game.add_text_actor( + "msg", + "Press any key to advance to the next music selection.\n\nIf you are not running with \"--release\", it may take several seconds for each song to load!", + ); + msg.translation.y = -200.0; + + game.add_logic(logic); + game.run(GameState { music_index: 0 }); +} + +fn logic(engine_state: &mut EngineState, game_state: &mut GameState) -> bool { + let mut should_play_new_song = false; + // Play a new song because a key was pressed + for ev in engine_state.keyboard_events.drain(..) { + if ev.state != ElementState::Pressed { + continue; + } + game_state.music_index = (game_state.music_index + 1) % MusicPreset::variant_iter().count(); + should_play_new_song = true; + break; + } + + if should_play_new_song || !engine_state.audio_manager.music_playing() { + // Actually play the new song + let music_preset = MusicPreset::variant_iter() + .nth(game_state.music_index) + .unwrap(); + engine_state.audio_manager.play_music(music_preset, 1.0); + // And make a text actor saying what song we're playing + let note1 = engine_state.add_text_actor("note1", format!("Looping: {:?}", music_preset)); + note1.font_size = 75.0; + } + true +} diff --git a/examples/sfx.rs b/examples/sfx.rs index 1c3f5a2..16933ce 100644 --- a/examples/sfx.rs +++ b/examples/sfx.rs @@ -1,61 +1,26 @@ -use rusty_engine::prelude::*; +//! This is an example of playing a sound effect preset. For playing your own sound effect file, +//! please see the `sound` example. -#[derive(Default)] -struct GameState { - sfx_timers: Vec, - end_timer: Timer, -} +use rusty_engine::prelude::*; -rusty_engine::init!(GameState); +rusty_engine::init!(); fn main() { let mut game = Game::new(); - let mut game_state = GameState::default(); - - // One timer to launch each sound effect - for (i, _sfx) in SfxPreset::variant_iter().enumerate() { - game_state - .sfx_timers - .push(Timer::from_seconds((i as f32) * 2.0, false)); - } - // One timer to end them all - game_state.end_timer = - Timer::from_seconds((SfxPreset::variant_iter().len() as f32) * 2.0 + 1.0, false); - - let mut msg = game.add_text_actor("msg", "Playing sound effects!"); - msg.translation = Vec2::new(0.0, 100.0); - msg.font_size = 60.0; - - let mut sfx_label = game.add_text_actor("sfx_label", ""); - sfx_label.translation = Vec2::new(0.0, -100.0); - sfx_label.font_size = 90.0; - - game.add_logic(logic); - game.run(game_state); -} - -fn logic(engine_state: &mut EngineState, game_state: &mut GameState) -> bool { - for (i, timer) in game_state.sfx_timers.iter_mut().enumerate() { - // None of the timers repeat, and they're all set to different times, so when the timer in - // index X goes off, play sound effect in index X - if timer.tick(engine_state.delta).just_finished() { - // Play a new sound effect - let sfx = SfxPreset::variant_iter().nth(i).unwrap(); - engine_state.audio_manager.play_sfx(sfx, 1.0); - // Update the text to show which sound effect we are playing - let sfx_label = engine_state.text_actors.get_mut("sfx_label").unwrap(); - sfx_label.text = format!("{:?}", sfx); - } - } - - // Are we all done? - if game_state - .end_timer - .tick(engine_state.delta) - .just_finished() - { - let sfx_label = engine_state.text_actors.get_mut("sfx_label").unwrap(); - sfx_label.text = "That's all! Press Esc to quit.".into(); - } - true + let msg = game.add_text_actor( + "msg", + "You can play sound effect presets that are included in the asset pack. For example:", + ); + msg.translation.y = 50.0; + + let msg2 = game.add_text_actor_with_font( + "msg2", + "engine_state.audio_manager.play_sfx(SfxPreset::Jingle1, 1.0);", + "FiraMono-Medium.ttf", + ); + msg2.translation.y = -50.0; + + game.audio_manager.play_sfx(SfxPreset::Jingle1, 1.0); + + game.run(()); } diff --git a/examples/sfx_sampler.rs b/examples/sfx_sampler.rs new file mode 100644 index 0000000..1c3f5a2 --- /dev/null +++ b/examples/sfx_sampler.rs @@ -0,0 +1,61 @@ +use rusty_engine::prelude::*; + +#[derive(Default)] +struct GameState { + sfx_timers: Vec, + end_timer: Timer, +} + +rusty_engine::init!(GameState); + +fn main() { + let mut game = Game::new(); + let mut game_state = GameState::default(); + + // One timer to launch each sound effect + for (i, _sfx) in SfxPreset::variant_iter().enumerate() { + game_state + .sfx_timers + .push(Timer::from_seconds((i as f32) * 2.0, false)); + } + // One timer to end them all + game_state.end_timer = + Timer::from_seconds((SfxPreset::variant_iter().len() as f32) * 2.0 + 1.0, false); + + let mut msg = game.add_text_actor("msg", "Playing sound effects!"); + msg.translation = Vec2::new(0.0, 100.0); + msg.font_size = 60.0; + + let mut sfx_label = game.add_text_actor("sfx_label", ""); + sfx_label.translation = Vec2::new(0.0, -100.0); + sfx_label.font_size = 90.0; + + game.add_logic(logic); + game.run(game_state); +} + +fn logic(engine_state: &mut EngineState, game_state: &mut GameState) -> bool { + for (i, timer) in game_state.sfx_timers.iter_mut().enumerate() { + // None of the timers repeat, and they're all set to different times, so when the timer in + // index X goes off, play sound effect in index X + if timer.tick(engine_state.delta).just_finished() { + // Play a new sound effect + let sfx = SfxPreset::variant_iter().nth(i).unwrap(); + engine_state.audio_manager.play_sfx(sfx, 1.0); + // Update the text to show which sound effect we are playing + let sfx_label = engine_state.text_actors.get_mut("sfx_label").unwrap(); + sfx_label.text = format!("{:?}", sfx); + } + } + + // Are we all done? + if game_state + .end_timer + .tick(engine_state.delta) + .just_finished() + { + let sfx_label = engine_state.text_actors.get_mut("sfx_label").unwrap(); + sfx_label.text = "That's all! Press Esc to quit.".into(); + } + true +} diff --git a/examples/sound.rs b/examples/sound.rs new file mode 100644 index 0000000..0147925 --- /dev/null +++ b/examples/sound.rs @@ -0,0 +1,26 @@ +//! This is an example of playing sound by path. For playing music or sound effect presets, please +//! see the `music` or `sfx` examples. + +use rusty_engine::prelude::*; + +rusty_engine::init!(); + +fn main() { + let mut game = Game::new(); + let msg = game.add_text_actor( + "msg", + "You can add your own sound files to the assets/audio directory (or its\nsubdirectories) and play them by relative path. For example:", + ); + msg.translation.y = 100.0; + + let msg2 = game.add_text_actor_with_font( + "msg2", + "engine_state.audio_manager.play_sfx(\"sfx/congratulations.ogg\", 1.0);", + "FiraMono-Medium.ttf", + ); + msg2.translation.y = -100.0; + + game.audio_manager.play_sfx("sfx/congratulations.ogg", 1.0); + + game.run(()); +} diff --git a/src/actor.rs b/src/actor.rs index b3afb35..5ce55a7 100644 --- a/src/actor.rs +++ b/src/actor.rs @@ -122,7 +122,7 @@ impl ActorPreset { } /// Build a usable actor from this preset. This is called for you if you use - /// [`GameState::add_actor`]. + /// [`EngineState::add_actor`](crate::prelude::EngineState::add_actor). pub fn build(self, label: String) -> Actor { let filename = self.filename(); let collider = self.collider(); diff --git a/src/audio.rs b/src/audio.rs index e23aed3..5d9e583 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -1,49 +1,117 @@ +//! Facilities for interacting with audio, including: [`AudioManager`], [`MusicPreset`], and +//! [`SfxPreset`] +//! +//! You may add your own sound files to the `assets/audio` directory or any of its subdirectories +//! and play them as sound effects or music by providing the relative path to the file. For example, +//! if you place a file named `my_sound_effect.mp3` in this directory, you could play it with: +//! +//! ```rust +//! # use rusty_engine::prelude::*; +//! # +//! # rusty_engine::init!(); +//! # +//! # fn main() { +//! # let mut engine_state = Game::new(); +//! // Inside your logic function... +//! engine_state.audio_manager.play_sfx("my_sound_effect.mp3", 1.0); +//! # } +//! ``` +//! +//! Or, if you create a `my_game/` subdirectory and place a file named `spooky_loop.ogg`, you could play it as continuous music with: +//! +//! ```rust +//! # use rusty_engine::prelude::*; +//! # +//! # rusty_engine::init!(); +//! # +//! # fn main() { +//! # let mut engine_state = Game::new(); +//! // Inside your logic function... +//! engine_state.audio_manager.play_music("my_game/spooky_loop.ogg", 1.0); +//! # } +//! ``` +//! +//! The sound effects provided in this asset pack have convenient `enum`s defined that you can use instead of a path to the file: `SfxPreset` and `MusicPreset`. For example: +//! +//! ```rust +//! // Import the enums into scope first +//! use rusty_engine::prelude::*; +//! +//! # rusty_engine::init!(); +//! # +//! # fn main() { +//! # let mut engine_state = Game::new(); +//! // Inside your logic function... +//! engine_state.audio_manager.play_sfx(SfxPreset::Confirmation1, 1.0); +//! engine_state.audio_manager.play_music(MusicPreset::Classy8Bit, 1.0); +//! # } +//! ``` +//! + use crate::prelude::EngineState; use bevy::prelude::*; use bevy_kira_audio::{Audio, AudioChannel}; use std::array::IntoIter; +#[derive(Default)] +#[doc(hidden)] +/// Use a Bevy plugin to run a Bevy system to handle our audio logic +pub struct AudioManagerPlugin; + +impl Plugin for AudioManagerPlugin { + fn build(&self, app: &mut bevy::prelude::AppBuilder) { + app.add_system(queue_managed_audio_system.system()); + } +} + +/// You will interact with a [`AudioManager`] for all audio needs in Rusty Engine. It is exposed +/// through the [`EngineState`](crate::prelude::EngineState) struct provided to your logic function +/// each frame as the [`audio_manager`](crate::prelude::EngineState::audio_manager) field. #[derive(Debug, Default)] pub struct AudioManager { - sfx_queue: Vec<(SfxPreset, f32)>, - music_queue: Vec>, + sfx_queue: Vec<(String, f32)>, + music_queue: Vec>, playing: AudioChannel, music_playing: bool, } impl AudioManager { - /// Play a sound, `volume` ranges from `0.0` to `1.0`. - pub fn play_sfx(&mut self, sfx_preset: SfxPreset, volume: f32) { - self.sfx_queue.push((sfx_preset, volume.clamp(0.0, 1.0))); + /// Play a sound effect. `volume` ranges from `0.0` to `1.0`. `sfx` can be an [`SfxPreset`] or a + /// string containing the relative path/filename of a sound file within the `assets/audio` + /// directory. Sound effects are "fire and forget". They will play to completion and then stop. + /// Multiple sound effects will be mixed and play simultaneously. + pub fn play_sfx>(&mut self, sfx: S, volume: f32) { + self.sfx_queue.push((sfx.into(), volume.clamp(0.0, 1.0))); } - /// Play looping music. `volume` ranges from `0.0` to `1.0`. Any music already playing will be - /// stopped. - pub fn play_music(&mut self, music_preset: MusicPreset, volume: f32) { + /// Play looping music. `volume` ranges from `0.0` to `1.0`. Music will loop until stopped with + /// [`stop_music`](AudioManager::stop_music). Playing music stops any previously playing music. + /// `music` can be a [`MusicPreset`] or a string containing the relative path/filename of a + /// sound file within the `assets/audio` directory. + pub fn play_music>(&mut self, music: S, volume: f32) { self.music_playing = true; self.music_queue - .push(Some((music_preset, volume.clamp(0.0, 1.0)))); + .push(Some((music.into(), volume.clamp(0.0, 1.0)))); } - /// Stop any music currently playing + /// Stop any music currently playing. Ignored if no music is currently playing. pub fn stop_music(&mut self) { - self.music_playing = false; - self.music_queue.push(None); + if self.music_playing { + self.music_playing = false; + self.music_queue.push(None); + } } - /// Whether music is currently playing + /// Whether music is currently playing. pub fn music_playing(&self) -> bool { self.music_playing } } -#[derive(Default)] -pub struct AudioManagerPlugin; - -impl Plugin for AudioManagerPlugin { - fn build(&self, app: &mut bevy::prelude::AppBuilder) { - app.add_system(queue_managed_audio_system.system()); - } -} - #[derive(Copy, Clone, Debug)] +/// Sound effects included with the downloadable asset pack. You can hear these all played in the +/// `sfx` example by cloning the `rusty_engine` repository and running the following command: +/// +/// ```text +/// cargo run --release --example sfx +/// ``` pub enum SfxPreset { Click, Confirmation1, @@ -66,29 +134,6 @@ pub enum SfxPreset { } impl SfxPreset { - fn to_path(self) -> &'static str { - match self { - SfxPreset::Click => "audio/sfx/click.ogg", - SfxPreset::Confirmation1 => "audio/sfx/confirmation1.ogg", - SfxPreset::Confirmation2 => "audio/sfx/confirmation2.ogg", - SfxPreset::Congratulations => "audio/sfx/congratulations.ogg", - SfxPreset::Forcefield1 => "audio/sfx/forcefield1.ogg", - SfxPreset::Forcefield2 => "audio/sfx/forcefield2.ogg", - SfxPreset::Impact1 => "audio/sfx/impact1.ogg", - SfxPreset::Impact2 => "audio/sfx/impact2.ogg", - SfxPreset::Impact3 => "audio/sfx/impact3.ogg", - SfxPreset::Jingle1 => "audio/sfx/jingle1.ogg", - SfxPreset::Jingle2 => "audio/sfx/jingle2.ogg", - SfxPreset::Jingle3 => "audio/sfx/jingle3.ogg", - SfxPreset::Minimize1 => "audio/sfx/minimize1.ogg", - SfxPreset::Minimize2 => "audio/sfx/minimize2.ogg", - SfxPreset::Switch1 => "audio/sfx/switch1.ogg", - SfxPreset::Switch2 => "audio/sfx/switch2.ogg", - SfxPreset::Tones1 => "audio/sfx/tones1.ogg", - SfxPreset::Tones2 => "audio/sfx/tones2.ogg", - } - } - pub fn variant_iter() -> IntoIter { static SFX_PRESETS: [SfxPreset; 18] = [ SfxPreset::Click, @@ -114,6 +159,37 @@ impl SfxPreset { } } +impl From for String { + fn from(sfx_preset: SfxPreset) -> Self { + match sfx_preset { + SfxPreset::Click => "sfx/click.ogg".into(), + SfxPreset::Confirmation1 => "sfx/confirmation1.ogg".into(), + SfxPreset::Confirmation2 => "sfx/confirmation2.ogg".into(), + SfxPreset::Congratulations => "sfx/congratulations.ogg".into(), + SfxPreset::Forcefield1 => "sfx/forcefield1.ogg".into(), + SfxPreset::Forcefield2 => "sfx/forcefield2.ogg".into(), + SfxPreset::Impact1 => "sfx/impact1.ogg".into(), + SfxPreset::Impact2 => "sfx/impact2.ogg".into(), + SfxPreset::Impact3 => "sfx/impact3.ogg".into(), + SfxPreset::Jingle1 => "sfx/jingle1.ogg".into(), + SfxPreset::Jingle2 => "sfx/jingle2.ogg".into(), + SfxPreset::Jingle3 => "sfx/jingle3.ogg".into(), + SfxPreset::Minimize1 => "sfx/minimize1.ogg".into(), + SfxPreset::Minimize2 => "sfx/minimize2.ogg".into(), + SfxPreset::Switch1 => "sfx/switch1.ogg".into(), + SfxPreset::Switch2 => "sfx/switch2.ogg".into(), + SfxPreset::Tones1 => "sfx/tones1.ogg".into(), + SfxPreset::Tones2 => "sfx/tones2.ogg".into(), + } + } +} + +/// Music included with the downloadable asset pack. You can hear this music in the `music` example +/// by cloning the `rusty_engine` repository and running the following command: +/// +/// ```text +/// cargo run --release --example music +/// ``` #[derive(Copy, Clone, Debug)] pub enum MusicPreset { Classy8Bit, @@ -122,14 +198,6 @@ pub enum MusicPreset { } impl MusicPreset { - fn to_path(self) -> &'static str { - match self { - MusicPreset::Classy8Bit => "audio/music/Classy 8-Bit.oga", - MusicPreset::MysteriousMagic => "audio/music/Mysterious Magic.oga", - MusicPreset::WhimsicalPopsicle => "audio/music/Whimsical Popsicle.oga", - } - } - pub fn variant_iter() -> IntoIter { static MUSIC_PRESETS: [MusicPreset; 3] = [ MusicPreset::Classy8Bit, @@ -140,20 +208,32 @@ impl MusicPreset { } } +impl From for String { + fn from(music_preset: MusicPreset) -> String { + match music_preset { + MusicPreset::Classy8Bit => "music/Classy 8-Bit.oga".into(), + MusicPreset::MysteriousMagic => "music/Mysterious Magic.oga".into(), + MusicPreset::WhimsicalPopsicle => "music/Whimsical Popsicle.oga".into(), + } + } +} + +// The Bevy system that checks and see if there is any audio management that needs to be done. +#[doc(hidden)] pub fn queue_managed_audio_system( asset_server: Res, audio: Res