Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add audio settings #909

Merged
merged 10 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions assets/game.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ main_menu:
menu_width: 350

default_settings:
main_volume: 1.0
matchmaking_server: matchmaker.bones.fishfolk.org:65534
player_controls:
# Gamepad controls
Expand Down
7 changes: 7 additions & 0 deletions assets/locales/en-US/settings.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ action = Action
networking = Networking
matchmaking-server = Matchmaking Server

# Audio settings
audio = Audio
volume = Volume
volume-main = Main
volume-music = Music
volume-effects = Effects

# Graphics settings
graphics = Graphics
fullscreen = Fullscreen
Expand Down
210 changes: 210 additions & 0 deletions src/audio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
use std::collections::VecDeque;

use bones_framework::prelude::kira::{
sound::{
static_sound::{StaticSoundHandle, StaticSoundSettings},
PlaybackState,
},
tween::{self, Tween},
Volume,
};

use crate::prelude::*;

pub mod music;

pub use music::*;

pub fn game_plugin(game: &mut Game) {
game.init_shared_resource::<AudioCenter>();

let session = game.sessions.create(SessionNames::AUDIO);

// Audio doesn't do any rendering
session.visible = false;
session
.stages
.add_system_to_stage(First, music_system)
.add_system_to_stage(First, process_audio_events)
.add_system_to_stage(Last, kill_finished_audios);
}

/// A resource that can be used to control game audios.
#[derive(HasSchema)]
#[schema(no_clone)]
pub struct AudioCenter {
/// Buffer for audio events that have not yet been processed.
events: VecDeque<AudioEvent>,
/// The handle to the current music.
music: Option<Audio>,
}

impl Default for AudioCenter {
fn default() -> Self {
Self {
events: VecDeque::with_capacity(16),
music: None,
}
}
}

impl AudioCenter {
/// Push an audio event to the queue for later processing.
pub fn event(&mut self, event: AudioEvent) {
self.events.push_back(event);
}

/// Get the playback state of the music.
pub fn music_state(&self) -> Option<PlaybackState> {
self.music.as_ref().map(|m| m.handle.state())
}

/// Play a sound. These are usually short audios that indicate something
/// happened in game, e.g. a player jump, an explosion, etc.
pub fn play_sound(&mut self, sound_source: Handle<AudioSource>, volume: f64) {
self.events.push_back(AudioEvent::PlaySound {
sound_source,
volume,
})
}

/// Play some music. These may or may not loop.
///
/// Any current music is stopped.
pub fn play_music(
&mut self,
sound_source: Handle<AudioSource>,
sound_settings: StaticSoundSettings,
) {
self.events.push_back(AudioEvent::PlayMusic {
sound_source,
sound_settings: Box::new(sound_settings),
});
}
}

/// An audio event that may be sent to the [`AudioCenter`] resource for
/// processing.
#[derive(Clone, Debug)]
pub enum AudioEvent {
/// Update the volume of all audios using the new values.
VolumeChange {
main_volume: f64,
music_volume: f64,
effects_volume: f64,
},
/// Play some music.
///
/// Any current music is stopped.
PlayMusic {
/// The handle for the music.
sound_source: Handle<AudioSource>,
/// The settings for the music.
sound_settings: Box<StaticSoundSettings>,
},
/// Play a sound.
PlaySound {
/// The handle to the sound to play.
sound_source: Handle<AudioSource>,
/// The volume to play the sound at.
volume: f64,
},
}

#[derive(HasSchema)]
#[schema(no_clone, no_default, opaque)]
#[repr(C)]
pub struct Audio {
/// The handle for the audio.
handle: StaticSoundHandle,
/// The original volume requested for the audio.
volume: f64,
}

fn process_audio_events(
mut audio_manager: ResMut<AudioManager>,
mut audio_center: ResMut<AudioCenter>,
assets: ResInit<AssetServer>,
mut entities: ResMut<Entities>,
mut audios: CompMut<Audio>,
storage: Res<Storage>,
) {
let settings = storage.get::<Settings>().unwrap();

for event in audio_center.events.drain(..).collect::<Vec<_>>() {
match event {
AudioEvent::VolumeChange {
main_volume,
music_volume,
effects_volume,
} => {
let tween = Tween::default();
// Update music volume
if let Some(music) = &mut audio_center.music {
let volume = main_volume * music_volume * music.volume;
if let Err(err) = music.handle.set_volume(volume, tween) {
warn!("Error setting music volume: {err}");
}
}
// Update sound volumes
for audio in audios.iter_mut() {
let volume = main_volume * effects_volume * audio.volume;
if let Err(err) = audio.handle.set_volume(volume, tween) {
warn!("Error setting audio volume: {err}");
}
}
}
AudioEvent::PlayMusic {
sound_source,
mut sound_settings,
} => {
// Stop the current music
if let Some(mut music) = audio_center.music.take() {
let tween = Tween {
start_time: kira::StartTime::Immediate,
duration: MUSIC_FADE_DURATION,
easing: tween::Easing::Linear,
};
music.handle.stop(tween).unwrap();
}
// Scale the requested volume by the settings value
let volume = match sound_settings.volume {
tween::Value::Fixed(vol) => vol.as_amplitude(),
_ => MUSIC_VOLUME,
};
let scaled_volume = settings.main_volume * settings.music_volume * volume;
sound_settings.volume = tween::Value::Fixed(Volume::Amplitude(scaled_volume));
// Play the new music
let sound_data = assets.get(sound_source).with_settings(*sound_settings);
match audio_manager.play(sound_data) {
Err(err) => warn!("Error playing music: {err}"),
Ok(handle) => audio_center.music = Some(Audio { handle, volume }),
}
}
AudioEvent::PlaySound {
sound_source,
volume,
} => {
let scaled_volume = settings.main_volume * settings.effects_volume * volume;
let sound_data = assets
.get(sound_source)
.with_settings(StaticSoundSettings::default().volume(scaled_volume));
match audio_manager.play(sound_data) {
Err(err) => warn!("Error playing sound: {err}"),
Ok(handle) => {
let audio_ent = entities.create();
audios.insert(audio_ent, Audio { handle, volume });
}
}
}
}
}
}

fn kill_finished_audios(entities: Res<Entities>, audios: Comp<Audio>, mut commands: Commands) {
for (audio_ent, audio) in entities.iter_with(&audios) {
if audio.handle.state() == PlaybackState::Stopped {
commands.add(move |mut entities: ResMut<Entities>| entities.kill(audio_ent));
}
}
}
101 changes: 101 additions & 0 deletions src/audio/music.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use bones_framework::prelude::kira::{
sound::{static_sound::StaticSoundSettings, PlaybackState, Region},
tween::Tween,
};

use crate::{prelude::*, ui::main_menu::MenuPage};

/// The music playback state.
#[derive(HasSchema, Default, PartialEq, Eq)]
#[schema(no_clone)]
pub enum MusicState {
/// Music is not playing.
#[default]
None,
/// Playing the main menu music.
MainMenu,
/// Playing the character select music.
CharacterSelect,
/// Playing the credits music.
Credits,
/// Playing the fight music.
Fight {
/// The index of the song in the shuffled playlist.
idx: usize,
},
}

/// Bevy resource containing the in-game music playlist shuffled.
#[derive(HasSchema, Deref, DerefMut, Clone, Default)]
#[repr(C)]
pub struct ShuffledPlaylist(pub SVec<Handle<AudioSource>>);

/// The amount of time to spend fading the music in and out.
pub const MUSIC_FADE_DURATION: Duration = Duration::from_millis(500);

pub const MUSIC_VOLUME: f64 = 0.1;

/// System that plays music according to the game mode.
pub(super) fn music_system(
meta: Root<GameMeta>,
mut audio: ResMut<AudioCenter>,
mut shuffled_fight_music: ResMutInit<ShuffledPlaylist>,
mut music_state: ResMutInit<MusicState>,
ctx: Res<EguiCtx>,
sessions: Res<Sessions>,
) {
if shuffled_fight_music.is_empty() {
let mut songs = meta.music.fight.clone();
THREAD_RNG.with(|rng| rng.shuffle(&mut songs));
**shuffled_fight_music = songs;
}

let tween = Tween {
start_time: kira::StartTime::Immediate,
duration: MUSIC_FADE_DURATION,
easing: kira::tween::Easing::Linear,
};
let play_settings = StaticSoundSettings::default()
.volume(MUSIC_VOLUME)
.fade_in_tween(tween);

// If we are in a game
if sessions.get(SessionNames::GAME).is_some() {
if let MusicState::Fight { idx } = &mut *music_state {
if let Some(PlaybackState::Stopped) = audio.music_state() {
*idx = (*idx + 1) % shuffled_fight_music.len();
audio.play_music(shuffled_fight_music[*idx], play_settings);
}
} else if let Some(song) = shuffled_fight_music.get(0) {
audio.play_music(*song, play_settings);
*music_state = MusicState::Fight { idx: 0 };
}

// If we are on a menu page
} else if sessions.get(SessionNames::MAIN_MENU).is_some() {
let menu_page = ctx.get_state::<MenuPage>();
match menu_page {
MenuPage::PlayerSelect | MenuPage::MapSelect { .. } | MenuPage::NetworkGame => {
if *music_state != MusicState::CharacterSelect {
audio.play_music(
meta.music.title_screen,
play_settings.loop_region(Region::default()),
);
*music_state = MusicState::CharacterSelect;
}
}
MenuPage::Home | MenuPage::Settings => {
if *music_state != MusicState::MainMenu {
audio.play_music(meta.music.title_screen, play_settings);
*music_state = MusicState::MainMenu;
}
}
MenuPage::Credits => {
if *music_state != MusicState::Credits {
audio.play_music(meta.music.credits, play_settings);
*music_state = MusicState::Credits;
}
}
}
}
}
6 changes: 2 additions & 4 deletions src/core.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
pub mod attachment;
pub mod audio;
pub mod bullet;
pub mod camera;
pub mod damage;
Expand Down Expand Up @@ -30,7 +29,7 @@ use crate::{prelude::*, settings::PlayerControlMapping};

pub mod prelude {
pub use super::{
attachment::*, audio::*, bullet::*, camera::*, damage::*, debug::*, editor::*, editor::*,
attachment::*, bullet::*, camera::*, damage::*, debug::*, editor::*, editor::*,
elements::prelude::*, flappy_jellyfish::*, globals::*, input::*, item::*, lifetime::*,
map::*, map_constructor::*, metadata::*, physics::*, player::*, random::*, utils::*, FPS,
MAX_PLAYERS,
Expand Down Expand Up @@ -67,8 +66,7 @@ impl SessionPlugin for MatchPlugin {
fn install(self, session: &mut Session) {
session
.install_plugin(DefaultSessionPlugin)
.install_plugin(LuaPluginLoaderSessionPlugin(self.plugins))
.install_plugin(audio::session_plugin);
.install_plugin(LuaPluginLoaderSessionPlugin(self.plugins));

physics::install(session);
input::install(session);
Expand Down
Loading
Loading