use std::{collections::HashMap, fmt::Debug, hash::Hash, time::Duration}; use bevy::{asset::HandleId, prelude::*, transform::TransformSystem}; use bevy_openal::{Buffer, Context, Sound, SoundState}; use rand::random; use crate::{ commands::RunIfExistsExt, core::{CoreConfig, Player, PointLike}, exploration::ExplorationFocused, visibility::VisibleEntities, }; #[derive(Component, Clone, Debug, Reflect)] #[reflect(Component)] pub struct Footstep { pub sound: HandleId, pub step_length: f32, pub gain: f32, pub reference_distance: f32, pub max_distance: f32, pub rolloff_factor: f32, pub pitch: f32, pub pitch_variation: Option<f32>, } impl Default for Footstep { fn default() -> Self { Self { sound: "".into(), step_length: 0.8, gain: 1., reference_distance: 1., max_distance: f32::MAX, rolloff_factor: 1., pitch: 1., pitch_variation: Some(0.15), } } } #[derive(Component, Clone, Debug)] pub struct SoundIcon { pub sound: HandleId, pub gain: f32, pub pitch: f32, pub reference_distance: f32, pub max_distance: f32, pub rolloff_factor: f32, pub interval: Option<Timer>, } impl Default for SoundIcon { fn default() -> Self { let seconds = random::<f32>() + 4.5; let mut icon = Self { sound: "".into(), gain: 0.3, pitch: 1., reference_distance: 1., max_distance: f32::MAX, rolloff_factor: 1., interval: Some(Timer::from_seconds(seconds, true)), }; if let Some(ref mut interval) = icon.interval { let seconds = Duration::from_secs_f32(seconds - 0.1); interval.set_elapsed(seconds); } icon } } #[derive(Bundle, Default)] pub struct FootstepBundle { pub footstep: Footstep, pub transform: Transform, pub global_transform: GlobalTransform, } #[derive(Bundle, Clone, Debug, Default)] pub struct SoundIconBundle { pub sound_icon: SoundIcon, pub transform: Transform, pub global_transform: GlobalTransform, } fn add_footstep_sounds( mut commands: Commands, footsteps: Query<(Entity, &Footstep), Added<Footstep>>, assets: Res<Assets<Buffer>>, ) { for (entity, footstep) in footsteps.iter() { let buffer = assets.get_handle(footstep.sound); let gain = footstep.gain; commands.run_if_exists(entity, move |mut entity| { entity.insert(Sound { buffer, state: SoundState::Stopped, gain, ..default() }); }); } } fn footstep( mut last_step_distance: Local<HashMap<Entity, (f32, Transform)>>, mut footsteps: Query<(Entity, &Footstep, &Parent, &mut Sound), Changed<GlobalTransform>>, transforms_storage: Query<&Transform>, ) { for (entity, footstep, parent, mut sound) in footsteps.iter_mut() { let coordinates = transforms_storage.get(**parent).unwrap(); if let Some(last) = last_step_distance.get(&entity) { let distance = last.0 + (last.1.distance(coordinates)); if distance >= footstep.step_length { last_step_distance.insert(entity, (0., *coordinates)); sound.gain = footstep.gain; sound.reference_distance = footstep.reference_distance; sound.max_distance = footstep.max_distance; sound.rolloff_factor = footstep.rolloff_factor; sound.pitch = footstep.pitch; if let Some(pitch_variation) = footstep.pitch_variation { let mut pitch = footstep.pitch - pitch_variation / 2.; pitch += random::<f32>() * pitch_variation; sound.pitch = pitch; } sound.play(); } else if last.1 != *coordinates { last_step_distance.insert(entity, (distance, *coordinates)); } } else { last_step_distance.insert(entity, (0., *coordinates)); } } } fn add_sound_icon_sounds( mut commands: Commands, icons: Query<(Entity, &SoundIcon), Added<SoundIcon>>, assets: Res<Assets<Buffer>>, ) { for (entity, icon) in icons.iter() { let buffer = assets.get_handle(icon.sound); let gain = icon.gain; let pitch = icon.pitch; let looping = icon.interval.is_none(); let reference_distance = icon.reference_distance; let max_distance = icon.max_distance; let rolloff_factor = icon.rolloff_factor; commands.run_if_exists(entity, move |mut entity| { entity.insert(Sound { buffer, gain, pitch, looping, state: SoundState::Stopped, reference_distance, max_distance, rolloff_factor, ..default() }); }); } } fn sound_icon<S>( config: Res<SoundConfig<S>>, state: Res<State<S>>, time: Res<Time>, viewers: Query<&VisibleEntities, With<Player>>, mut icons: Query<( Entity, &mut SoundIcon, Option<&Transform>, Option<&Parent>, &mut Sound, )>, buffers: Res<Assets<Buffer>>, ) where S: 'static + Clone + Debug + Eq + Hash + Send + Sync, { if !(*config).sound_icon_states.is_empty() && !config.sound_icon_states.contains(state.current()) { return; } for visible in viewers.iter() { for (icon_entity, mut icon, coordinates, parent, mut sound) in icons.iter_mut() { let entity = if coordinates.is_some() { icon_entity } else { **parent.unwrap() }; if visible.contains(&entity) { let looping = sound.looping; if looping { sound.state = SoundState::Playing; } else if let Some(interval) = icon.interval.as_mut() { interval.tick(time.delta()); if interval.finished() { sound.state = SoundState::Playing; interval.reset(); } } let buffer = buffers.get_handle(icon.sound); sound.looping = icon.interval.is_none(); if sound.buffer != buffer { sound.buffer = buffer; } sound.gain = icon.gain; sound.pitch = icon.pitch; sound.reference_distance = icon.reference_distance; sound.max_distance = icon.max_distance; sound.rolloff_factor = icon.rolloff_factor; } else { sound.state = SoundState::Stopped; } } } } fn sound_icon_exploration_focus_changed( mut focused: Query<(Entity, Option<&Children>), Changed<ExplorationFocused>>, mut icons: Query<&mut SoundIcon>, ) { const ICON_GAIN: f32 = 3.; for (entity, children) in focused.iter_mut() { if let Ok(mut icon) = icons.get_mut(entity) { icon.gain *= ICON_GAIN; } if let Some(children) = children { for child in children.iter() { if let Ok(mut icon) = icons.get_mut(*child) { icon.gain *= ICON_GAIN; } } } } } fn sound_icon_exploration_focus_removed( removed: RemovedComponents<ExplorationFocused>, mut query: Query<&mut SoundIcon>, children: Query<&Children>, ) { const ICON_GAIN: f32 = 3.; for entity in removed.iter() { if let Ok(mut icon) = query.get_mut(entity) { icon.gain /= ICON_GAIN; } if let Ok(children) = children.get(entity) { for child in children.iter() { if let Ok(mut icon) = query.get_mut(*child) { icon.gain *= ICON_GAIN; } } } } } fn scale_sounds(config: Res<CoreConfig>, mut sounds: Query<&mut Sound>) { let pixels_per_unit = config.pixels_per_unit as f32; for mut sound in sounds.iter_mut() { sound.reference_distance *= pixels_per_unit; if sound.max_distance != f32::MAX { sound.max_distance *= pixels_per_unit; } } } #[derive(Clone, Debug)] pub struct SoundConfig<S> { pub sound_icon_states: Vec<S>, } impl<S> Default for SoundConfig<S> { fn default() -> Self { Self { sound_icon_states: vec![], } } } pub struct SoundPlugin<'a, S>(std::marker::PhantomData<&'a S>); impl<'a, S> Default for SoundPlugin<'a, S> { fn default() -> Self { Self(std::marker::PhantomData) } } impl<'a, S> Plugin for SoundPlugin<'a, S> where S: 'static + Clone + Debug + Eq + Hash + Send + Sync, 'a: 'static, { fn build(&self, app: &mut App) { if !app.world.contains_resource::<SoundConfig<S>>() { app.insert_resource(SoundConfig::<S>::default()); } const SOUND_ICON_AND_EXPLORATION_STAGE: &str = "sound_icon_and_exploration"; let core_config = *app.world.get_resource::<CoreConfig>().unwrap(); if let Some(context) = app.world.get_resource::<Context>() { context .set_meters_per_unit(1. / core_config.pixels_per_unit as f32) .unwrap(); } app.register_type::<Footstep>() .add_system(add_footstep_sounds) .add_system_to_stage( CoreStage::PostUpdate, footstep.after(TransformSystem::TransformPropagate), ) .add_system(add_sound_icon_sounds) .add_system_to_stage( CoreStage::PostUpdate, sound_icon::<S>.after(TransformSystem::TransformPropagate), ) .add_stage_after( CoreStage::PostUpdate, SOUND_ICON_AND_EXPLORATION_STAGE, SystemStage::parallel(), ) .add_system_to_stage( SOUND_ICON_AND_EXPLORATION_STAGE, sound_icon_exploration_focus_changed, ) .add_system_to_stage( SOUND_ICON_AND_EXPLORATION_STAGE, sound_icon_exploration_focus_removed, ) .add_system(scale_sounds); } }