diff --git a/korangar/src/loaders/action/mod.rs b/korangar/src/loaders/action/mod.rs index 1bb14cb4..729d606a 100644 --- a/korangar/src/loaders/action/mod.rs +++ b/korangar/src/loaders/action/mod.rs @@ -4,9 +4,10 @@ use std::sync::Arc; use cgmath::{Array, Vector2}; use derive_new::new; +use korangar_audio::{AudioEngine, SoundEffectKey}; #[cfg(feature = "debug")] use korangar_debug::logging::{print_debug, Colorize, Timer}; -use korangar_interface::elements::PrototypeElement; +use korangar_interface::elements::{ElementCell, PrototypeElement}; use korangar_util::container::{Cacheable, SimpleCache}; use korangar_util::FileLoader; use ragnarok_bytes::{ByteReader, FromBytes}; @@ -25,6 +26,11 @@ use crate::renderer::SpriteRenderer; const MAX_CACHE_COUNT: u32 = 256; const MAX_CACHE_SIZE: usize = 64 * 1024 * 1024; +// TODO: NHA The numeric value of action types are based on the EntityType! +// For example "Dead" is 8 for the PC and 4 for a monster. +// This means we need to refactor the AnimationState, so that the mouse +// uses a different animation state struct (since we can't do simple usize +// conversions). #[derive(Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum ActionType { #[default] @@ -39,6 +45,26 @@ impl From for usize { } } +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub enum ActionEvent { + /// Start playing a WAV sound file. + Sound { key: SoundEffectKey }, + /// An attack event when the "flinch" animation is played. + Attack, + /// Start playing a WAV sound file. + Unknown, +} + +impl PrototypeElement for ActionEvent { + fn to_element(&self, display: String) -> ElementCell { + match self { + Self::Sound { .. } => PrototypeElement::to_element(&"Sound", display), + Self::Attack => PrototypeElement::to_element(&"Attack", display), + Self::Unknown => PrototypeElement::to_element(&"Unknown", display), + } + } +} + #[derive(Clone, Debug, new)] pub struct AnimationState { pub action: T, @@ -100,6 +126,8 @@ impl AnimationState { pub struct Actions { pub actions: Vec, pub delays: Vec, + #[hidden_element] + pub events: Vec, #[cfg(feature = "debug")] actions_data: ActionsData, } @@ -118,9 +146,9 @@ impl Actions { T: Into + Copy, { let direction = camera_direction % 8; - let aa = animation_state.action.into() * 8 + direction; - let a = &self.actions[aa % self.actions.len()]; - let delay = self.delays[aa % self.delays.len()]; + let animation_action = animation_state.action.into() * 8 + direction; + let action = &self.actions[animation_action % self.actions.len()]; + let delay = self.delays[animation_action % self.delays.len()]; let factor = animation_state .factor @@ -129,14 +157,14 @@ impl Actions { let frame = animation_state .duration - .map(|duration| animation_state.time * a.motions.len() as u32 / duration) + .map(|duration| animation_state.time * action.motions.len() as u32 / duration) .unwrap_or_else(|| (animation_state.time as f32 / factor) as u32); - // TODO: work out how to avoid losing digits when casting timg to an f32. When + // TODO: work out how to avoid losing digits when casting timing to an f32. When // fixed remove set_start_time in MouseCursor. - let fs = &a.motions[frame as usize % a.motions.len()]; + let motion = &action.motions[frame as usize % action.motions.len()]; - for sprite_clip in &fs.sprite_clips { + for sprite_clip in &motion.sprite_clips { // `get` instead of a direct index in case a fallback was loaded let Some(texture) = sprite.textures.get(sprite_clip.sprite_number as usize) else { return; @@ -186,13 +214,15 @@ impl Cacheable for Actions { pub struct ActionLoader { game_file_loader: Arc, + audio_engine: Arc>, cache: SimpleCache>, } impl ActionLoader { - pub fn new(game_file_loader: Arc) -> Self { + pub fn new(game_file_loader: Arc, audio_engine: Arc>) -> Self { Self { game_file_loader, + audio_engine, cache: SimpleCache::new( NonZeroU32::new(MAX_CACHE_COUNT).unwrap(), NonZeroUsize::new(MAX_CACHE_SIZE).unwrap(), @@ -231,6 +261,24 @@ impl ActionLoader { } }; + let events: Vec = actions_data + .events + .iter() + .enumerate() + .map(|(_index, event)| { + if event.name.ends_with(".wav") { + let key = self.audio_engine.load(&event.name); + ActionEvent::Sound { key } + } else if event.name == "atk" || event.name == "atk.txt" { + ActionEvent::Attack + } else { + #[cfg(feature = "debug")] + print_debug!("Found unknown event at index `{}`: {:?}", _index, event.name); + ActionEvent::Unknown + } + }) + .collect(); + #[cfg(feature = "debug")] let saved_actions_data = actions_data.clone(); @@ -241,6 +289,7 @@ impl ActionLoader { let sprite = Arc::new(Actions { actions: actions_data.actions, delays, + events, #[cfg(feature = "debug")] actions_data: saved_actions_data, }); diff --git a/korangar/src/loaders/animation/mod.rs b/korangar/src/loaders/animation/mod.rs index a3e71b4e..02da31b3 100644 --- a/korangar/src/loaders/animation/mod.rs +++ b/korangar/src/loaders/animation/mod.rs @@ -9,7 +9,7 @@ use korangar_util::container::SimpleCache; use num::Zero; use super::error::LoadError; -use crate::loaders::{ActionLoader, SpriteLoader}; +use crate::loaders::{ActionEvent, ActionLoader, SpriteLoader}; use crate::world::{Animation, AnimationData, AnimationFrame, AnimationFramePart, AnimationPair}; use crate::{Color, EntityType}; @@ -155,7 +155,18 @@ impl AnimationLoader { color, ..Default::default() }; + + let event: Option = if let Some(event_id) = motion.event_id + && event_id != -1 + && let Some(event) = animation_pair.actions.events.get(event_id as usize).copied() + { + Some(event) + } else { + None + }; + let frame = AnimationFrame { + event, size, top_left: Vector2::zero(), offset, @@ -423,7 +434,9 @@ fn merge_frame(frames: &mut [AnimationFrame]) -> AnimationFrame { }, ..Default::default() }; + let frame = AnimationFrame { + event: None, size: Vector2::new(1, 1), top_left: Vector2::zero(), offset: Vector2::zero(), @@ -433,6 +446,7 @@ fn merge_frame(frames: &mut [AnimationFrame]) -> AnimationFrame { #[cfg(feature = "debug")] vertical_matrix: Matrix4::identity(), }; + return frame; } @@ -454,6 +468,8 @@ fn merge_frame(frames: &mut [AnimationFrame]) -> AnimationFrame { new_frame_parts.append(&mut frame.frame_parts); } + let event = frames.iter().filter_map(|frame| frame.event).next(); + // The origin is set at (0,0). // // The top-left point of the rectangle is calculated as @@ -465,6 +481,7 @@ fn merge_frame(frames: &mut [AnimationFrame]) -> AnimationFrame { // The new offset is calculated as // center_point - origin. AnimationFrame { + event, size: Vector2::new(new_width, new_height), top_left: Vector2::zero(), offset: Vector2::new(top_left_x + (new_width - 1) / 2, top_left_y + (new_height - 1) / 2), diff --git a/korangar/src/main.rs b/korangar/src/main.rs index 128445bb..908487bc 100644 --- a/korangar/src/main.rs +++ b/korangar/src/main.rs @@ -370,6 +370,7 @@ impl Client { let mute_on_focus_loss = audio_settings.mapped(|settings| &settings.mute_on_focus_loss).new_remote(); let audio_engine = Arc::new(AudioEngine::new(game_file_loader.clone())); + audio_engine.set_background_music_volume(0.1); }); time_phase!("create resource managers", { @@ -380,7 +381,7 @@ impl Client { let font_loader = Rc::new(RefCell::new(FontLoader::new(&game_file_loader, &texture_loader))); let mut map_loader = MapLoader::new(device.clone(), queue.clone(), game_file_loader.clone(), audio_engine.clone()); let mut sprite_loader = SpriteLoader::new(device.clone(), queue.clone(), game_file_loader.clone()); - let mut action_loader = ActionLoader::new(game_file_loader.clone()); + let mut action_loader = ActionLoader::new(game_file_loader.clone(), audio_engine.clone()); let effect_loader = EffectLoader::new(game_file_loader.clone()); let animation_loader = AnimationLoader::new(); @@ -1835,65 +1836,47 @@ impl Client { user_event_measurement.stop(); #[cfg(feature = "debug")] - let update_entities_measurement = Profiler::start_measurement("update entities"); - - self.entities - .iter_mut() - .for_each(|entity| entity.update(&self.map, delta_time as f32, client_tick)); + let update_main_camera_measurement = Profiler::start_measurement("update main camera"); - #[cfg(feature = "debug")] - update_entities_measurement.stop(); + let window_size = self.graphics_engine.get_window_size(); + let screen_size: ScreenSize = window_size.into(); - if !self.entities.is_empty() { - let player_position = self.entities[0].get_position(); - self.player_camera.set_smoothed_focus_point(player_position); - self.directional_shadow_camera.set_focus_point(self.player_camera.focus_point()); + if self.entities.is_empty() { + self.start_camera.update(delta_time); + self.start_camera.generate_view_projection(window_size); + } else { + self.player_camera.update(delta_time); + self.player_camera.generate_view_projection(window_size); } #[cfg(feature = "debug")] - let update_cameras_measurement = Profiler::start_measurement("update cameras"); - - let lighting_mode = *self.lighting_mode.get(); - let shadow_quality = *self.shadow_quality.get(); - let ambient_light_color = self.map.get_ambient_light_color(lighting_mode, day_timer); - let (directional_light_direction, directional_light_color) = self.map.get_directional_light(lighting_mode, day_timer); - - self.start_camera.update(delta_time); - self.player_camera.update(delta_time); - - let view_direction = match self.entities.is_empty() { - true => self.start_camera.view_direction(), - false => self.player_camera.view_direction(), - }; - - self.directional_shadow_camera.update(directional_light_direction, view_direction); + if self.render_settings.get().use_debug_camera { + self.debug_camera.generate_view_projection(window_size); + } #[cfg(feature = "debug")] - update_cameras_measurement.stop(); - - self.particle_holder.update(delta_time as f32); - self.effect_holder.update(&self.entities, delta_time as f32); - - let (clear_interface, render_interface) = self - .interface - .update(&self.application, self.font_loader.clone(), &mut self.focus_state); - self.mouse_cursor.update(client_tick); + update_main_camera_measurement.stop(); #[cfg(feature = "debug")] - let matrices_measurement = Profiler::start_measurement("generate view and projection matrices"); + let update_entities_measurement = Profiler::start_measurement("update entities"); - let window_size = self.graphics_engine.get_window_size(); - let screen_size: ScreenSize = window_size.into(); + { + let current_camera: &(dyn Camera + Send + Sync) = match self.entities.is_empty() { + #[cfg(feature = "debug")] + _ if self.render_settings.get().use_debug_camera => &self.debug_camera, + true => &self.start_camera, + false => &self.player_camera, + }; - if self.entities.is_empty() { - self.start_camera.generate_view_projection(window_size); + self.entities + .iter_mut() + .for_each(|entity| entity.update(&self.audio_engine, &self.map, current_camera, client_tick)); } - self.player_camera.generate_view_projection(window_size); - self.directional_shadow_camera.generate_view_projection(window_size); - #[cfg(feature = "debug")] - if self.render_settings.get().use_debug_camera { - self.debug_camera.generate_view_projection(window_size); + if !self.entities.is_empty() { + let player_position = self.entities[0].get_position(); + self.player_camera.set_smoothed_focus_point(player_position); + self.directional_shadow_camera.set_focus_point(self.player_camera.focus_point()); } let current_camera: &(dyn Camera + Send + Sync) = match self.entities.is_empty() { @@ -1906,12 +1889,27 @@ impl Client { let (view_matrix, projection_matrix) = current_camera.view_projection_matrices(); let camera_position = current_camera.camera_position().to_homogeneous(); + #[cfg(feature = "debug")] + update_entities_measurement.stop(); + + #[cfg(feature = "debug")] + let update_shadow_camera_measurement = Profiler::start_measurement("update directional shadow camera"); + + let lighting_mode = *self.lighting_mode.get(); + let shadow_quality = *self.shadow_quality.get(); + let ambient_light_color = self.map.get_ambient_light_color(lighting_mode, day_timer); + let (directional_light_direction, directional_light_color) = self.map.get_directional_light(lighting_mode, day_timer); + + self.directional_shadow_camera + .update(directional_light_direction, current_camera.view_direction()); + self.directional_shadow_camera.generate_view_projection(window_size); + let (directional_light_view_matrix, directional_light_projection_matrix) = self.directional_shadow_camera.view_projection_matrices(); let directional_light_view_projection_matrix = directional_light_projection_matrix * directional_light_view_matrix; #[cfg(feature = "debug")] - matrices_measurement.stop(); + update_shadow_camera_measurement.stop(); #[cfg(feature = "debug")] let frame_measurement = Profiler::start_measurement("update audio engine"); @@ -1921,7 +1919,7 @@ impl Client { let listener = current_camera.focus_point() + EAR_HEIGHT; self.audio_engine - .set_ambient_listener(listener, current_camera.view_direction(), current_camera.look_up_vector()); + .set_spatial_listener(listener, current_camera.view_direction(), current_camera.look_up_vector()); self.audio_engine.update(); #[cfg(feature = "debug")] @@ -1930,10 +1928,18 @@ impl Client { #[cfg(feature = "debug")] let prepare_frame_measurement = Profiler::start_measurement("prepare frame"); + self.particle_holder.update(delta_time as f32); + self.effect_holder.update(&self.entities, delta_time as f32); + + let (clear_interface, render_interface) = self + .interface + .update(&self.application, self.font_loader.clone(), &mut self.focus_state); + self.mouse_cursor.update(client_tick); + #[cfg(feature = "debug")] let render_settings = &*self.render_settings.get(); let walk_indicator_color = self.application.get_game_theme().indicator.walking.get(); - let entities = &self.entities[..]; + #[cfg(feature = "debug")] let hovered_marker_identifier = match mouse_target { Some(PickerTarget::Marker(marker_identifier)) => Some(marker_identifier), @@ -1981,7 +1987,7 @@ impl Client { &mut self.debug_marker_renderer, current_camera, render_settings, - entities, + &self.entities, &point_light_set, hovered_marker_identifier, ); @@ -1991,7 +1997,7 @@ impl Client { &mut self.middle_interface_renderer, current_camera, render_settings, - entities, + &self.entities, &point_light_set, hovered_marker_identifier, ); @@ -2048,7 +2054,7 @@ impl Client { #[cfg_attr(feature = "debug", korangar_debug::debug_condition(render_settings.show_entities))] self.map.render_entities( &mut self.directional_shadow_entity_instructions, - entities, + &mut self.entities, &self.directional_shadow_camera, ); } @@ -2119,12 +2125,13 @@ impl Client { }; #[cfg_attr(feature = "debug", korangar_debug::debug_condition(render_settings.show_entities))] - self.map.render_entities(&mut self.entity_instructions, entities, entity_camera); + self.map + .render_entities(&mut self.entity_instructions, &mut self.entities, entity_camera); #[cfg(feature = "debug")] if render_settings.show_entities_debug { self.map - .render_entities_debug(&mut self.rectangle_instructions, entities, entity_camera); + .render_entities_debug(&mut self.rectangle_instructions, &self.entities, entity_camera); } #[cfg_attr(feature = "debug", korangar_debug::debug_condition(render_settings.show_water))] @@ -2158,13 +2165,18 @@ impl Client { ); } - self.particle_holder - .render(&self.bottom_interface_renderer, current_camera, screen_size, scaling, entities); + self.particle_holder.render( + &self.bottom_interface_renderer, + current_camera, + screen_size, + scaling, + &self.entities, + ); self.effect_holder.render(&mut self.effect_renderer, current_camera); if let Some(PickerTarget::Tile { x, y }) = mouse_target - && !entities.is_empty() + && !&self.entities.is_empty() { #[cfg_attr(feature = "debug", korangar_debug::debug_condition(render_settings.show_indicators))] self.map.render_walk_indicator( @@ -2173,7 +2185,7 @@ impl Client { Vector2::new(x as usize, y as usize), ); } else if let Some(PickerTarget::Entity(entity_id)) = mouse_target { - let entity = entities.iter().find(|entity| entity.get_entity_id() == entity_id); + let entity = &self.entities.iter().find(|entity| entity.get_entity_id() == entity_id); if let Some(entity) = entity { entity.render_status( @@ -2199,11 +2211,11 @@ impl Client { } } - if !entities.is_empty() { + if !&self.entities.is_empty() { #[cfg(feature = "debug")] profile_block!("render player status"); - entities[0].render_status( + self.entities[0].render_status( &self.middle_interface_renderer, current_camera, self.application.get_game_theme(), diff --git a/korangar/src/world/animation/mod.rs b/korangar/src/world/animation/mod.rs index 4f4b2afb..3ac2055e 100644 --- a/korangar/src/world/animation/mod.rs +++ b/korangar/src/world/animation/mod.rs @@ -8,7 +8,7 @@ use ragnarok_packets::EntityId; #[cfg(feature = "debug")] use crate::graphics::DebugRectangleInstruction; use crate::graphics::{Color, EntityInstruction}; -use crate::loaders::{ActionType, Actions, AnimationState, Sprite}; +use crate::loaders::{ActionEvent, ActionType, Actions, AnimationState, Sprite}; use crate::world::{Camera, EntityType}; const TILE_SIZE: f32 = 10.0; @@ -43,6 +43,7 @@ pub struct Animation { #[derive(Clone)] pub struct AnimationFrame { + pub event: Option, pub offset: Vector2, pub top_left: Vector2, pub size: Vector2, @@ -84,9 +85,13 @@ impl AnimationData { pub fn get_frame(&self, animation_state: &AnimationState, camera: &dyn Camera, head_direction: usize) -> &AnimationFrame { let camera_direction = camera.camera_direction(); let direction = (camera_direction + head_direction) % 8; - let aa = animation_state.action as usize * 8 + direction; - let delay = self.delays[aa % self.delays.len()]; - let animation = &self.animations[aa % self.animations.len()]; + let animation_action_index = animation_state.action as usize * 8 + direction; + + let delay_index = animation_action_index % self.delays.len(); + let animation_index = animation_action_index % self.animations.len(); + + let delay = self.delays[delay_index]; + let animation = &self.animations[animation_index]; let factor = animation_state .factor @@ -100,14 +105,14 @@ impl AnimationData { // TODO: Work out how to avoid losing digits when casting time to an f32. When // fixed remove set_start_time in MouseCursor. - let time = frame_time as usize % animation.frames.len(); - let mut frame = &animation.frames[time]; + let frame_index = frame_time as usize % animation.frames.len(); // Remove Doridori animation from Player if self.entity_type == EntityType::Player && animation_state.action == ActionType::Idle { - frame = &animation.frames[0]; + &animation.frames[0] + } else { + &animation.frames[frame_index] } - frame } pub fn calculate_world_matrix(&self, camera: &dyn Camera, frame: &AnimationFrame, entity_position: Point3) -> Matrix4 { @@ -134,11 +139,11 @@ impl AnimationData { &self, instructions: &mut Vec, camera: &dyn Camera, + add_to_picker: bool, entity_id: EntityId, entity_position: Point3, animation_state: &AnimationState, head_direction: usize, - add_to_picker: bool, ) { let frame = self.get_frame(animation_state, camera, head_direction); let world_matrix = self.calculate_world_matrix(camera, frame, entity_position); diff --git a/korangar/src/world/entity/mod.rs b/korangar/src/world/entity/mod.rs index 50da1b8a..b9e37727 100644 --- a/korangar/src/world/entity/mod.rs +++ b/korangar/src/world/entity/mod.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use arrayvec::ArrayVec; use cgmath::{EuclideanSpace, Point3, Vector2, VectorSpace}; use derive_new::new; +use korangar_audio::{AudioEngine, SoundEffectKey}; use korangar_interface::elements::PrototypeElement; use korangar_interface::windows::{PrototypeWindow, Window}; use korangar_networking::EntityData; @@ -21,7 +22,7 @@ use crate::interface::application::InterfaceSettings; use crate::interface::layout::{ScreenPosition, ScreenSize}; use crate::interface::theme::GameTheme; use crate::interface::windows::WindowCache; -use crate::loaders::{ActionLoader, ActionType, AnimationLoader, AnimationState, ScriptLoader, SpriteLoader}; +use crate::loaders::{ActionEvent, ActionLoader, ActionType, AnimationLoader, AnimationState, GameFileLoader, ScriptLoader, SpriteLoader}; use crate::renderer::GameInterfaceRenderer; #[cfg(feature = "debug")] use crate::renderer::MarkerRenderer; @@ -33,6 +34,8 @@ use crate::{Buffer, Color, ModelVertex}; const MALE_HAIR_LOOKUP: &[usize] = &[2, 2, 1, 7, 5, 4, 3, 6, 8, 9, 10, 12, 11]; const FEMALE_HAIR_LOOKUP: &[usize] = &[2, 2, 4, 7, 1, 5, 3, 6, 12, 10, 9, 11, 8]; +const SOUND_COOLDOWN_DURATION: u32 = 200; +const SPATIAL_SOUND_RANGE: f32 = 250.0; pub enum ResourceState { Available(T), @@ -75,6 +78,36 @@ pub enum EntityType { Monster, } +#[derive(Copy, Clone, Default)] +pub struct SoundState { + previous_key: Option, + last_played_at: Option, +} + +impl SoundState { + pub fn update( + &mut self, + audio_engine: &AudioEngine, + position: Point3, + sound_effect_key: SoundEffectKey, + client_tick: ClientTick, + ) { + let should_play = if Some(sound_effect_key) == self.previous_key + && let Some(last_tick) = self.last_played_at + { + (client_tick.0.saturating_sub(last_tick.0)) >= SOUND_COOLDOWN_DURATION + } else { + true + }; + + if should_play { + audio_engine.play_spatial_sound_effect(sound_effect_key, position, SPATIAL_SOUND_RANGE); + self.last_played_at = Some(client_tick); + self.previous_key = Some(sound_effect_key); + } + } +} + #[derive(PrototypeElement)] pub struct Common { pub entity_id: EntityId, @@ -95,6 +128,8 @@ pub struct Common { details: ResourceState, #[hidden_element] animation_state: AnimationState, + #[hidden_element] + sound_state: SoundState, } #[cfg_attr(feature = "debug", korangar_debug::profile)] @@ -350,6 +385,7 @@ impl Common { animation_data, details, animation_state, + sound_state: SoundState::default(), }; if let Some(destination) = entity_data.destination { @@ -374,14 +410,25 @@ impl Common { .unwrap(); } - pub fn set_position(&mut self, map: &Map, position: Vector2, client_tick: ClientTick) { - self.grid_position = position; - self.position = map.get_world_position(position); - self.active_movement = None; - self.animation_state.idle(client_tick); + pub fn update(&mut self, audio_engine: &AudioEngine, map: &Map, camera: &dyn Camera, client_tick: ClientTick) { + self.update_movement(map, client_tick); + self.animation_state.update(client_tick); + + let frame = self.animation_data.get_frame(&self.animation_state, camera, self.head_direction); + match frame.event { + Some(ActionEvent::Sound { key }) => { + self.sound_state.update(audio_engine, self.position, key, client_tick); + } + Some(ActionEvent::Attack) => { + // TODO: NHA What do we need to do at this event? Other clients + // are playing the attackers weapon attack sound using + // this event. + } + None | Some(ActionEvent::Unknown) => { /* Nothing to do */ } + } } - pub fn update(&mut self, map: &Map, _delta_time: f32, client_tick: ClientTick) { + fn update_movement(&mut self, map: &Map, client_tick: ClientTick) { if let Some(active_movement) = self.active_movement.take() { let last_step = active_movement.steps.last().unwrap(); @@ -428,8 +475,13 @@ impl Common { self.active_movement = active_movement.into(); } } + } - self.animation_state.update(client_tick); + fn set_position(&mut self, map: &Map, position: Vector2, client_tick: ClientTick) { + self.grid_position = position; + self.position = map.get_world_position(position); + self.active_movement = None; + self.animation_state.idle(client_tick); } pub fn move_from_to( @@ -691,7 +743,7 @@ impl Common { steps_vertex_buffer.write_exact(queue, pathing_vertices.as_slice()); } else { let vertex_buffer = Arc::new(Buffer::with_data( - &device, + device, queue, "pathing vertex buffer", BufferUsages::VERTEX | BufferUsages::COPY_DST, @@ -706,11 +758,11 @@ impl Common { self.animation_data.render( instructions, camera, + add_to_picker, self.entity_id, self.position, &self.animation_state, self.head_direction, - add_to_picker, ); } @@ -1072,8 +1124,8 @@ impl Entity { common.maximum_health_points = maximum_health_points; } - pub fn update(&mut self, map: &Map, delta_time: f32, client_tick: ClientTick) { - self.get_common_mut().update(map, delta_time, client_tick); + pub fn update(&mut self, audio_engine: &AudioEngine, map: &Map, camera: &dyn Camera, client_tick: ClientTick) { + self.get_common_mut().update(audio_engine, map, camera, client_tick); } pub fn move_from_to( diff --git a/korangar/src/world/map/mod.rs b/korangar/src/world/map/mod.rs index 4b91bc30..a95e1a91 100644 --- a/korangar/src/world/map/mod.rs +++ b/korangar/src/world/map/mod.rs @@ -305,7 +305,7 @@ impl Map { } #[cfg_attr(feature = "debug", korangar_debug::profile)] - pub fn render_entities(&self, instructions: &mut Vec, entities: &[Entity], camera: &dyn Camera) { + pub fn render_entities(&self, instructions: &mut Vec, entities: &mut [Entity], camera: &dyn Camera) { entities.iter().enumerate().for_each(|(index, entity)| { entity.render(instructions, camera, index != 0); }); diff --git a/korangar_audio/src/lib.rs b/korangar_audio/src/lib.rs index 63600607..fd941838 100644 --- a/korangar_audio/src/lib.rs +++ b/korangar_audio/src/lib.rs @@ -47,11 +47,17 @@ struct BackgroundMusicTrack { handle: StreamingSoundHandle, } +enum QueuedSoundEffectType { + Sound, + SpatialSound { position: Vector3, range: f32 }, + AmbientSound { ambient_key: AmbientKey }, +} + struct QueuedSoundEffect { /// The key of the sound that should be played. sound_effect_key: SoundEffectKey, - /// The optional key to the ambient sound emitter. - ambient: Option, + /// The type of the queued sound effect. + sound_type: QueuedSoundEffectType, /// The time this playback was queued. queued_time: Instant, } @@ -101,9 +107,9 @@ pub struct AudioEngine { struct EngineContext { active_emitters: HashMap, - ambient_listener: ListenerHandle, + spatial_listener: ListenerHandle, ambient_sound: SimpleSlab, - ambient_track: TrackHandle, + spatial_sound_effect_track: TrackHandle, async_response_receiver: Receiver, async_response_sender: Sender, background_music_track: TrackHandle, @@ -147,11 +153,15 @@ impl AudioEngine { .add_sub_track(TrackBuilder::new()) .expect("Can't create background music track"); let sound_effect_track = manager.add_sub_track(TrackBuilder::new()).expect("Can't create sound effect track"); - let ambient_track = manager.add_sub_track(TrackBuilder::new()).expect("Can't create ambient track"); + let spatial_sound_effect_track = manager + .add_sub_track(TrackBuilder::new()) + .expect("Can't create spatial sound effect track"); let position = Vector3::new(0.0, 0.0, 0.0); let orientation = Quaternion::new(0.0, 0.0, 0.0, 0.0); - let ambient_listener = scene - .add_listener(position, orientation, ListenerSettings { track: ambient_track.id() }) + let spatial_listener = scene + .add_listener(position, orientation, ListenerSettings { + track: spatial_sound_effect_track.id(), + }) .expect("Can't create ambient listener"); let loading_sound_effect = HashSet::new(); let cache = SimpleCache::new( @@ -166,9 +176,9 @@ impl AudioEngine { let engine_context = Mutex::new(EngineContext { active_emitters: HashMap::default(), - ambient_listener, + spatial_listener, ambient_sound: SimpleSlab::default(), - ambient_track, + spatial_sound_effect_track, async_response_receiver, async_response_sender, background_music_track, @@ -265,9 +275,9 @@ impl AudioEngine { self.engine_context.lock().unwrap().set_sound_effect_volume(volume) } - /// Sets the volume of ambient sounds. - pub fn set_ambient_volume(&self, volume: impl Into>) { - self.engine_context.lock().unwrap().set_ambient_volume(volume) + /// Sets the volume of spatial sound effects. + pub fn set_spatial_sound_effect_volume(&self, volume: impl Into>) { + self.engine_context.lock().unwrap().set_spatial_sound_effect_volume(volume) } /// Plays the background music track. Fades out the currently playing @@ -282,20 +292,32 @@ impl AudioEngine { self.engine_context.lock().unwrap().play_sound_effect(sound_effect_key) } - /// Sets the listener of the ambient sound. This is normally the camera's + /// Plays a spatial sound effect, which will get removed automatically once + /// it finishes playing. + pub fn play_spatial_sound_effect(&self, sound_effect_key: SoundEffectKey, position: Point3, range: f32) { + self.engine_context + .lock() + .unwrap() + .play_spatial_sound_effect(sound_effect_key, position, range); + } + + /// Sets the listener of the spatial sound. This is normally the camera's /// position and orientation. This should update each frame. - pub fn set_ambient_listener(&self, position: Point3, view_direction: Vector3, look_up: Vector3) { + pub fn set_spatial_listener(&self, position: Point3, view_direction: Vector3, look_up: Vector3) { self.engine_context .lock() .unwrap() - .set_ambient_listener(position, view_direction, look_up) + .set_spatial_listener(position, view_direction, look_up) } - /// Ambient sound loops and needs to be removed once the player it outside - /// the ambient sound range. + /// Adds a static, spatial sound, that is used for ambient sound inside the + /// world. /// /// [`prepare_ambient_sound_world()`] must be called once all ambient sound /// have been added. + /// + /// [`clear_ambient_sound()`] must be called if the "map" or "level" is + /// switched. pub fn add_ambient_sound( &self, sound_effect_key: SoundEffectKey, @@ -310,11 +332,6 @@ impl AudioEngine { .add_ambient_sound(sound_effect_key, position, range, volume, cycle) } - /// Removes an ambient sound. - pub fn remove_ambient_sound(&self, ambient_key: AmbientKey) { - self.engine_context.lock().unwrap().remove_ambient_sound(ambient_key) - } - /// Removes all ambient sound emitters from the spatial scene. pub fn clear_ambient_sound(&self) { self.engine_context.lock().unwrap().clear_ambient_sound() @@ -354,8 +371,8 @@ impl EngineContext { }); } - fn set_ambient_volume(&mut self, volume: impl Into>) { - self.ambient_track.set_volume(volume, Tween { + fn set_spatial_sound_effect_volume(&mut self, volume: impl Into>) { + self.spatial_sound_effect_track.set_volume(volume, Tween { duration: Duration::from_millis(500), ..Default::default() }); @@ -416,11 +433,56 @@ impl EngineContext { &self.sound_effect_paths, &mut self.queued_sound_effect, sound_effect_key, - None, + QueuedSoundEffectType::Sound, + ); + } + + fn play_spatial_sound_effect(&mut self, sound_effect_key: SoundEffectKey, position: Point3, range: f32) { + // Kira uses a RH coordinate system, so we need to convert our LH vectors. + let position = Vector3::new(position.x, position.y, -position.z); + + if let Some(data) = self + .cache + .get(&sound_effect_key) + .map(|cached_sound_effect| cached_sound_effect.0.clone()) + { + let settings = EmitterSettings { + distances: EmitterDistances { + min_distance: 5.0, + max_distance: range, + }, + attenuation_function: Some(Easing::Linear), + enable_spatialization: true, + persist_until_sounds_finish: true, + }; + + match self.scene.add_emitter(position, settings) { + Ok(emitter_handle) => { + let data = adjust_ambient_sound(data, &emitter_handle, 1.0); + + if let Err(_error) = self.manager.play(data) { + #[cfg(feature = "debug")] + print_debug!("[{}] can't play sound effect: {:?}", "error".red(), _error); + } + } + Err(_error) => { + #[cfg(feature = "debug")] + print_debug!("[{}] can't add spatial sound emitter: {:?}", "error".red(), _error); + } + }; + } + + queue_sound_effect_playback( + self.game_file_loader.clone(), + self.async_response_sender.clone(), + &self.sound_effect_paths, + &mut self.queued_sound_effect, + sound_effect_key, + QueuedSoundEffectType::SpatialSound { position, range }, ); } - fn set_ambient_listener(&mut self, position: Point3, view_direction: Vector3, look_up: Vector3) { + fn set_spatial_listener(&mut self, position: Point3, view_direction: Vector3, look_up: Vector3) { let listener = Sphere::new(position, 10.0); self.query_result.clear(); @@ -464,7 +526,7 @@ impl EngineContext { .get(&sound_effect_key) .map(|cached_sound_effect| cached_sound_effect.0.clone()) { - let data = adjust_ambient_sound(data, &emitter_handle, sound_config); + let data = adjust_ambient_sound(data, &emitter_handle, sound_config.volume); match self.manager.play(data.clone()) { Ok(handle) => { if let Some(cycle) = sound_config.cycle { @@ -488,7 +550,7 @@ impl EngineContext { &self.sound_effect_paths, &mut self.queued_sound_effect, sound_effect_key, - Some(ambient_key), + QueuedSoundEffectType::AmbientSound { ambient_key }, ); } @@ -525,8 +587,8 @@ impl EngineContext { duration: Duration::from_millis(50), ..Default::default() }; - self.ambient_listener.set_position(position, tween); - self.ambient_listener.set_orientation(orientation, tween); + self.spatial_listener.set_position(position, tween); + self.spatial_listener.set_orientation(orientation, tween); } } @@ -548,15 +610,6 @@ impl EngineContext { .expect("Ambient sound slab is full") } - fn remove_ambient_sound(&mut self, ambient_key: AmbientKey) { - let _ = self.ambient_sound.remove(ambient_key); - if let Some(emitter) = self.active_emitters.remove(&ambient_key) { - // An emitter is removed from the spatial scene by dropping it. We make this - // explicit to express our intent. - drop(emitter); - } - } - fn clear_ambient_sound(&mut self) { self.query_result.clear(); self.previous_query_result.clear(); @@ -648,18 +701,44 @@ impl EngineContext { return true; }; - match queued.ambient { - None => { + match queued.sound_type { + QueuedSoundEffectType::Sound => { if let Err(_error) = self.manager.play(data.output_destination(&self.sound_effect_track)) { #[cfg(feature = "debug")] print_debug!("[{}] can't play sound effect: {:?}", "error".red(), _error); } } - Some(ambient_key) => { + QueuedSoundEffectType::SpatialSound { position, range } => { + let settings = EmitterSettings { + distances: EmitterDistances { + min_distance: 5.0, + max_distance: range, + }, + attenuation_function: Some(Easing::Linear), + enable_spatialization: true, + persist_until_sounds_finish: true, + }; + + match self.scene.add_emitter(position, settings) { + Ok(emitter_handle) => { + let data = adjust_ambient_sound(data, &emitter_handle, 1.0); + + if let Err(_error) = self.manager.play(data) { + #[cfg(feature = "debug")] + print_debug!("[{}] can't play sound effect: {:?}", "error".red(), _error); + } + } + Err(_error) => { + #[cfg(feature = "debug")] + print_debug!("[{}] can't add spatial sound emitter: {:?}", "error".red(), _error); + } + }; + } + QueuedSoundEffectType::AmbientSound { ambient_key } => { if let Some(emitter_handle) = self.active_emitters.get(&ambient_key) && let Some(sound_config) = self.ambient_sound.get(ambient_key) { - let data = adjust_ambient_sound(data, emitter_handle, sound_config); + let data = adjust_ambient_sound(data, emitter_handle, sound_config.volume); match self.manager.play(data.clone()) { Ok(handle) => { if let Some(cycle) = sound_config.cycle { @@ -721,11 +800,7 @@ impl EngineContext { } }; - // TODO: NHA Remove volume offset once we have a proper volume control in place. - let data = data - .volume(Volume::Amplitude(0.1)) - .output_destination(&self.background_music_track) - .loop_region(..); + let data = data.output_destination(&self.background_music_track).loop_region(..); let handle = match self.manager.play(data) { Ok(handle) => handle, Err(_error) => { @@ -742,9 +817,9 @@ impl EngineContext { } } -fn adjust_ambient_sound(mut data: StaticSoundData, emitter_handle: &EmitterHandle, sound_config: &AmbientSoundConfig) -> StaticSoundData { +fn adjust_ambient_sound(mut data: StaticSoundData, emitter_handle: &EmitterHandle, volume: f32) -> StaticSoundData { // Kira does the volume mapping from linear to logarithmic for us. - data.settings.volume = Volume::Amplitude(sound_config.volume as f64).into(); + data.settings.volume = Volume::Amplitude(volume as f64).into(); data.output_destination(emitter_handle) } @@ -754,7 +829,7 @@ fn queue_sound_effect_playback( sound_effect_paths: &GenerationalSlab, queued_sound_effect: &mut Vec, sound_effect_key: SoundEffectKey, - ambient: Option, + queued_sound_effect_type: QueuedSoundEffectType, ) -> bool { let Some(path) = sound_effect_paths.get(sound_effect_key).cloned() else { // This case could happen, if the sound effect was queued for deletion. @@ -763,7 +838,7 @@ fn queue_sound_effect_playback( queued_sound_effect.push(QueuedSoundEffect { sound_effect_key, - ambient, + sound_type: queued_sound_effect_type, queued_time: Instant::now(), });