Added outcome sound effects, fixed directional sound, particle outcomes

This commit is contained in:
Joshua Barretto 2020-07-31 23:25:23 +01:00 committed by scott-c
parent 7c31baef6f
commit 8547cdd681
13 changed files with 136 additions and 102 deletions

View File

@ -106,6 +106,18 @@
"voxygen.audio.sfx.inventory.consumable.food",
],
threshold: 0.3,
)
),
Explosion: (
files: [
"voxygen.audio.sfx.glider_open",
],
threshold: 0.5,
),
ProjectileShot: (
files: [
"voxygen.audio.sfx.glider_open",
],
threshold: 0.5,
),
}
)

View File

@ -16,7 +16,10 @@
//! the channel capacity has been reached and all channels are occupied, a
//! warning is logged, and no sound is played.
use crate::audio::fader::{FadeDirection, Fader};
use crate::audio::{
fader::{FadeDirection, Fader},
Listener,
};
use rodio::{Device, Sample, Sink, Source, SpatialSink};
use vek::*;
@ -163,11 +166,13 @@ impl SfxChannel {
pub fn is_done(&self) -> bool { self.sink.empty() }
pub fn set_emitter_position(&mut self, pos: [f32; 3]) { self.sink.set_emitter_position(pos); }
pub fn set_pos(&mut self, pos: Vec3<f32>) { self.pos = pos; }
pub fn set_left_ear_position(&mut self, pos: [f32; 3]) { self.sink.set_left_ear_position(pos); }
pub fn update(&mut self, listener: &Listener) {
const FALLOFF: f32 = 0.13;
pub fn set_right_ear_position(&mut self, pos: [f32; 3]) {
self.sink.set_right_ear_position(pos);
self.sink.set_emitter_position(((self.pos - listener.pos) * FALLOFF).into_array());
self.sink.set_left_ear_position(listener.ear_left_rpos.into_array());
self.sink.set_right_ear_position(listener.ear_right_rpos.into_array());
}
}

View File

@ -16,7 +16,14 @@ use cpal::traits::DeviceTrait;
use rodio::{source::Source, Decoder, Device};
use vek::*;
const FALLOFF: f32 = 0.13;
#[derive(Default, Clone)]
pub struct Listener {
pos: Vec3<f32>,
ori: Vec3<f32>,
ear_left_rpos: Vec3<f32>,
ear_right_rpos: Vec3<f32>,
}
/// Holds information about the system audio devices and internal channels used
/// for sfx and music playback. An instance of `AudioFrontend` is used by
@ -34,11 +41,7 @@ pub struct AudioFrontend {
sfx_volume: f32,
music_volume: f32,
listener_pos: Vec3<f32>,
listener_ori: Vec3<f32>,
listener_ear_left: Vec3<f32>,
listener_ear_right: Vec3<f32>,
listener: Listener,
}
impl AudioFrontend {
@ -63,10 +66,8 @@ impl AudioFrontend {
sfx_channels,
sfx_volume: 1.0,
music_volume: 1.0,
listener_pos: Vec3::zero(),
listener_ori: Vec3::zero(),
listener_ear_left: Vec3::zero(),
listener_ear_right: Vec3::zero(),
listener: Listener::default(),
}
}
@ -81,10 +82,7 @@ impl AudioFrontend {
sfx_channels: Vec::new(),
sfx_volume: 1.0,
music_volume: 1.0,
listener_pos: Vec3::zero(),
listener_ori: Vec3::zero(),
listener_ear_left: Vec3::zero(),
listener_ear_right: Vec3::zero(),
listener: Listener::default(),
}
}
@ -146,20 +144,15 @@ impl AudioFrontend {
/// Play (once) an sfx file by file path at the give position and volume
pub fn play_sfx(&mut self, sound: &str, pos: Vec3<f32>, vol: Option<f32>) {
if self.audio_device.is_some() {
let calc_pos = ((pos - self.listener_pos) * FALLOFF).into_array();
let sound = self
.sound_cache
.load_sound(sound)
.amplify(vol.unwrap_or(1.0));
let left_ear = self.listener_ear_left.into_array();
let right_ear = self.listener_ear_right.into_array();
let listener = self.listener.clone();
if let Some(channel) = self.get_sfx_channel() {
channel.set_emitter_position(calc_pos);
channel.set_left_ear_position(left_ear);
channel.set_right_ear_position(right_ear);
channel.set_pos(pos);
channel.update(&listener);
channel.play(sound);
}
}
@ -174,27 +167,17 @@ impl AudioFrontend {
}
}
pub fn set_listener_pos(&mut self, pos: &Vec3<f32>, ori: &Vec3<f32>) {
self.listener_pos = *pos;
self.listener_ori = ori.normalized();
pub fn set_listener_pos(&mut self, pos: Vec3<f32>, ori: Vec3<f32>) {
self.listener.pos = pos;
self.listener.ori = ori.normalized();
let up = Vec3::new(0.0, 0.0, 1.0);
let pos_left = up.cross(self.listener_ori).normalized();
let pos_right = self.listener_ori.cross(up).normalized();
self.listener_ear_left = pos_left;
self.listener_ear_right = pos_right;
self.listener.ear_left_rpos = up.cross(self.listener.ori).normalized();
self.listener.ear_right_rpos = -up.cross(self.listener.ori).normalized();
for channel in self.sfx_channels.iter_mut() {
if !channel.is_done() {
// TODO: Update this to correctly determine the updated relative position of
// the SFX emitter when the player (listener) moves
// channel.set_emitter_position(
// ((channel.pos - self.listener_pos) * FALLOFF).into_array(),
// );
channel.set_left_ear_position(pos_left.into_array());
channel.set_right_ear_position(pos_right.into_array());
channel.update(&self.listener);
}
}
}

View File

@ -1,6 +1,9 @@
/// EventMapper::Combat watches the combat states of surrounding entities' and
/// emits sfx related to weapons and attacks/abilities
use crate::audio::sfx::{SfxEvent, SfxEventItem, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR};
use crate::{
audio::sfx::{SfxEvent, SfxEventItem, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR},
scene::Camera,
};
use super::EventMapper;
@ -15,7 +18,6 @@ use common::{
use hashbrown::HashMap;
use specs::{Entity as EcsEntity, Join, WorldExt};
use std::time::{Duration, Instant};
use vek::*;
#[derive(Clone)]
struct PreviousEntityState {
@ -39,16 +41,13 @@ pub struct CombatEventMapper {
}
impl EventMapper for CombatEventMapper {
fn maintain(&mut self, state: &State, player_entity: EcsEntity, triggers: &SfxTriggers) {
fn maintain(&mut self, state: &State, player_entity: specs::Entity, camera: &Camera, triggers: &SfxTriggers) {
let ecs = state.ecs();
let sfx_event_bus = ecs.read_resource::<EventBus<SfxEventItem>>();
let mut sfx_emitter = sfx_event_bus.emitter();
let player_position = ecs
.read_storage::<Pos>()
.get(player_entity)
.map_or(Vec3::zero(), |pos| pos.0);
let cam_pos = camera.dependents().cam_pos;
for (entity, pos, loadout, character) in (
&ecs.entities(),
@ -58,7 +57,7 @@ impl EventMapper for CombatEventMapper {
)
.join()
.filter(|(_, e_pos, ..)| {
(e_pos.0.distance_squared(player_position)) < SFX_DIST_LIMIT_SQR
(e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR
})
{
if let Some(character) = character {

View File

@ -8,10 +8,11 @@ use combat::CombatEventMapper;
use movement::MovementEventMapper;
use progression::ProgressionEventMapper;
use crate::scene::Camera;
use super::SfxTriggers;
trait EventMapper {
fn maintain(&mut self, state: &State, player_entity: specs::Entity, triggers: &SfxTriggers);
fn maintain(&mut self, state: &State, player_entity: specs::Entity, camera: &Camera, triggers: &SfxTriggers);
}
pub struct SfxEventMapper {
@ -33,10 +34,11 @@ impl SfxEventMapper {
&mut self,
state: &State,
player_entity: specs::Entity,
camera: &Camera,
triggers: &SfxTriggers,
) {
for mapper in &mut self.mappers {
mapper.maintain(state, player_entity, triggers);
mapper.maintain(state, player_entity, camera, triggers);
}
}
}

View File

@ -2,7 +2,10 @@
/// and triggers sfx related to running, climbing and gliding, at a volume
/// proportionate to the extity's size
use super::EventMapper;
use crate::audio::sfx::{SfxEvent, SfxEventItem, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR};
use crate::{
audio::sfx::{SfxEvent, SfxEventItem, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR},
scene::Camera,
};
use common::{
comp::{Body, CharacterState, PhysicsState, Pos, Vel},
event::EventBus,
@ -35,16 +38,13 @@ pub struct MovementEventMapper {
}
impl EventMapper for MovementEventMapper {
fn maintain(&mut self, state: &State, player_entity: EcsEntity, triggers: &SfxTriggers) {
fn maintain(&mut self, state: &State, player_entity: specs::Entity, camera: &Camera, triggers: &SfxTriggers) {
let ecs = state.ecs();
let sfx_event_bus = ecs.read_resource::<EventBus<SfxEventItem>>();
let mut sfx_emitter = sfx_event_bus.emitter();
let player_position = ecs
.read_storage::<Pos>()
.get(player_entity)
.map_or(Vec3::zero(), |pos| pos.0);
let cam_pos = camera.dependents().cam_pos;
for (entity, pos, vel, body, physics, character) in (
&ecs.entities(),
@ -56,7 +56,7 @@ impl EventMapper for MovementEventMapper {
)
.join()
.filter(|(_, e_pos, ..)| {
(e_pos.0.distance_squared(player_position)) < SFX_DIST_LIMIT_SQR
(e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR
})
{
if let Some(character) = character {

View File

@ -2,7 +2,10 @@
/// and triggers sfx for gaining experience and levelling up
use super::EventMapper;
use crate::audio::sfx::{SfxEvent, SfxEventItem, SfxTriggers};
use crate::{
audio::sfx::{SfxEvent, SfxEventItem, SfxTriggers},
scene::Camera,
};
use common::{comp::Stats, event::EventBus, state::State};
use specs::WorldExt;
@ -23,7 +26,7 @@ pub struct ProgressionEventMapper {
impl EventMapper for ProgressionEventMapper {
#[allow(clippy::op_ref)] // TODO: Pending review in #587
fn maintain(&mut self, state: &State, player_entity: specs::Entity, triggers: &SfxTriggers) {
fn maintain(&mut self, state: &State, player_entity: specs::Entity, _camera: &Camera, triggers: &SfxTriggers) {
let ecs = state.ecs();
let next_state = ecs.read_storage::<Stats>().get(player_entity).map_or(

View File

@ -83,17 +83,17 @@
mod event_mapper;
use crate::audio::AudioFrontend;
use crate::{audio::AudioFrontend, scene::Camera};
use common::{
assets,
comp::{
item::{ItemKind, ToolCategory},
item::{Consumable, ItemKind, ToolCategory},
CharacterAbilityType, InventoryUpdateEvent, Ori, Pos,
},
event::EventBus,
state::State,
outcome::Outcome,
state::State,
};
use event_mapper::SfxEventMapper;
use hashbrown::HashMap;
@ -147,6 +147,8 @@ pub enum SfxEvent {
Wield(ToolCategory),
Unwield(ToolCategory),
Inventory(SfxInventoryEvent),
Explosion,
ProjectileShot,
}
#[derive(Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
@ -189,13 +191,19 @@ impl From<&InventoryUpdateEvent> for SfxEvent {
}
}
impl TryFrom<Outcome> for SfxEventItem {
impl<'a> TryFrom<&'a Outcome> for SfxEventItem {
type Error = ();
fn try_from(outcome: Outcome) -> Result<Self, Self::Error> {
fn try_from(outcome: &'a Outcome) -> Result<Self, Self::Error> {
match outcome {
Outcome::Explosion { pos, power } => Ok(Self::new(SfxEvent::GliderOpen, Some(pos), Some((power / 10.0).min(1.0)))),
Outcome::ProjectileShot { pos, .. } => Ok(Self::new(SfxEvent::GliderOpen, Some(pos), None)),
Outcome::Explosion { pos, power } => Ok(Self::new(
SfxEvent::Explosion,
Some(*pos),
Some((*power / 10.0).min(1.0)),
)),
Outcome::ProjectileShot { pos, .. } => {
Ok(Self::new(SfxEvent::ProjectileShot, Some(*pos), None))
},
_ => Err(()),
}
}
@ -237,36 +245,25 @@ impl SfxMgr {
audio: &mut AudioFrontend,
state: &State,
player_entity: specs::Entity,
camera: &Camera,
) {
if !audio.sfx_enabled() {
return;
}
self.event_mapper
.maintain(state, player_entity, &self.triggers);
.maintain(state, player_entity, camera, &self.triggers);
let ecs = state.ecs();
let player_position = ecs
.read_storage::<Pos>()
.get(player_entity)
.map_or(Vec3::zero(), |pos| pos.0);
let player_ori = *ecs
.read_storage::<Ori>()
.get(player_entity)
.copied()
.unwrap_or_default()
.0;
audio.set_listener_pos(&player_position, &player_ori);
audio.set_listener_pos(camera.dependents().cam_pos, camera.dependents().cam_dir);
let events = ecs.read_resource::<EventBus<SfxEventItem>>().recv_all();
for event in events {
let position = match event.pos {
Some(pos) => pos,
_ => player_position,
_ => camera.dependents().cam_pos,
};
if let Some(item) = self.triggers.get_trigger(&event.sfx) {

View File

@ -29,6 +29,7 @@ pub struct Dependents {
pub view_mat: Mat4<f32>,
pub proj_mat: Mat4<f32>,
pub cam_pos: Vec3<f32>,
pub cam_dir: Vec3<f32>,
}
pub struct Camera {
@ -67,6 +68,7 @@ impl Camera {
view_mat: Mat4::identity(),
proj_mat: Mat4::identity(),
cam_pos: Vec3::zero(),
cam_dir: Vec3::unit_y(),
},
}
}
@ -104,6 +106,8 @@ impl Camera {
// TODO: Make this more efficient.
self.dependents.cam_pos = Vec3::from(self.dependents.view_mat.inverted() * Vec4::unit_w());
self.dependents.cam_dir = Vec3::from(self.dependents.view_mat.inverted() * -Vec4::unit_z());
}
pub fn frustum(&self) -> Frustum<f32> {

View File

@ -4,7 +4,7 @@ pub mod particle;
pub mod simple;
pub mod terrain;
use self::{
pub use self::{
camera::{Camera, CameraMode},
figure::FigureMgr,
particle::ParticleMgr,
@ -135,6 +135,9 @@ impl Scene {
/// Get a reference to the scene's particle manager.
pub fn particle_mgr(&self) -> &ParticleMgr { &self.particle_mgr }
/// Get a mutable reference to the scene's particle manager.
pub fn particle_mgr_mut(&mut self) -> &mut ParticleMgr { &mut self.particle_mgr }
/// Get a reference to the scene's figure manager.
pub fn figure_mgr(&self) -> &FigureMgr { &self.figure_mgr }
@ -273,6 +276,7 @@ impl Scene {
view_mat,
proj_mat,
cam_pos,
..
} = self.camera.dependents();
// Update chunk loaded distance smoothly for nice shader fog
@ -401,7 +405,7 @@ impl Scene {
// Maintain audio
self.sfx_mgr
.maintain(audio, scene_data.state, scene_data.player_entity);
.maintain(audio, scene_data.state, scene_data.player_entity, &self.camera);
self.music_mgr.maintain(audio, scene_data.state);
}

View File

@ -10,13 +10,14 @@ use common::{
assets,
comp::{object, Body, CharacterState, Pos},
figure::Segment,
outcome::Outcome,
};
use dot_vox::DotVoxData;
use hashbrown::HashMap;
use rand::Rng;
use specs::{Join, WorldExt};
use std::time::{Duration, Instant};
use vek::Vec3;
use vek::*;
struct Particles {
alive_until: Instant, // created_at + lifespan
@ -45,6 +46,24 @@ impl ParticleMgr {
pub fn particle_count_visible(&self) -> usize { self.instances.count() }
pub fn handle_outcome(&mut self, outcome: &Outcome, scene_data: &SceneData) {
let time = scene_data.state.get_time();
let now = Instant::now();
let mut rng = rand::thread_rng();
match outcome {
Outcome::Explosion { pos, power } => {
for _ in 0..64 {
self.particles.push(Particles {
alive_until: now + Duration::from_secs(4),
instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, *pos + Vec2::<f32>::zero().map(|_| rng.gen_range(-1.0, 1.0) * power)),
});
}
},
_ => {},
}
}
pub fn maintain(&mut self, renderer: &mut Renderer, scene_data: &SceneData) {
if scene_data.particles_enabled {
let now = Instant::now();

View File

@ -171,6 +171,7 @@ impl Scene {
view_mat,
proj_mat,
cam_pos,
..
} = self.camera.dependents();
const VD: f32 = 115.0; // View Distance
const TIME: f64 = 43200.0; // 12 hours*3600 seconds

View File

@ -24,6 +24,7 @@ use common::{
terrain::{Block, BlockKind},
util::Dir,
vol::ReadVol,
outcome::Outcome,
};
use specs::{Join, WorldExt};
use std::{cell::RefCell, rc::Rc, time::Duration, convert::TryFrom};
@ -100,7 +101,7 @@ impl SessionState {
}
/// Tick the session (and the client attached to it).
fn tick(&mut self, dt: Duration, global_state: &mut GlobalState) -> Result<TickAction, Error> {
fn tick(&mut self, dt: Duration, global_state: &mut GlobalState, outcomes: &mut Vec<Outcome>) -> Result<TickAction, Error> {
self.inputs.tick(dt);
let mut client = self.client.borrow_mut();
@ -158,15 +159,7 @@ impl SessionState {
global_state.settings.graphics.view_distance = vd;
global_state.settings.save_to_file_warn();
},
client::Event::Outcome(outcome) => {
if let Ok(sfx_event_item) = SfxEventItem::try_from(outcome) {
client
.state()
.ecs()
.read_resource::<EventBus<SfxEventItem>>()
.emit_now(sfx_event_item);
}
},
client::Event::Outcome(outcome) => outcomes.push(outcome),
}
}
@ -218,7 +211,7 @@ impl PlayState for SessionState {
.camera_mut()
.compute_dependents(&*self.client.borrow().state().terrain());
let camera::Dependents {
view_mat, cam_pos, ..
cam_pos, cam_dir, ..
} = self.scene.camera().dependents();
let (is_aiming, aim_dir_offset) = {
@ -241,8 +234,6 @@ impl PlayState for SessionState {
};
self.is_aiming = is_aiming;
let cam_dir: Vec3<f32> = Vec3::from(view_mat.inverted() * -Vec4::unit_z());
// Check to see whether we're aiming at anything
let (build_pos, select_pos, target_entity) =
under_cursor(&self.client.borrow(), cam_pos, cam_dir);
@ -642,10 +633,12 @@ impl PlayState for SessionState {
self.inputs.climb = self.key_state.climb();
let mut outcomes = Vec::new();
// Runs if either in a multiplayer server or the singleplayer server is unpaused
if !global_state.paused() {
// Perform an in-game tick.
match self.tick(global_state.clock.get_avg_delta(), global_state) {
match self.tick(global_state.clock.get_avg_delta(), global_state, &mut outcomes) {
Ok(TickAction::Continue) => {}, // Do nothing
Ok(TickAction::Disconnect) => return PlayStateResult::Pop, // Go to main menu
Err(err) => {
@ -1030,6 +1023,18 @@ impl PlayState for SessionState {
&mut global_state.audio,
&scene_data,
);
// Process outcomes from client
for outcome in outcomes {
if let Ok(sfx_event_item) = SfxEventItem::try_from(&outcome) {
client
.state()
.ecs()
.read_resource::<EventBus<SfxEventItem>>()
.emit_now(sfx_event_item);
}
self.scene.particle_mgr_mut().handle_outcome(&outcome, &scene_data);
}
}
}