mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
SFX system
This is an event based approach to SFX sounds. There is a specific character sound event mapper which determines sfx to play based on character or NPC state, as well as emitting sfx events for non-character-triggers such as levelling up.
This commit is contained in:
parent
2fabf10758
commit
1c607c0a0c
67
assets/voxygen/audio/sfx.ron
Normal file
67
assets/voxygen/audio/sfx.ron
Normal file
@ -0,0 +1,67 @@
|
||||
(
|
||||
items: [
|
||||
(
|
||||
trigger: Run,
|
||||
files: [
|
||||
"voxygen.audio.sfx.footsteps.stepdirt_1",
|
||||
"voxygen.audio.sfx.footsteps.stepdirt_2",
|
||||
"voxygen.audio.sfx.footsteps.stepdirt_3",
|
||||
"voxygen.audio.sfx.footsteps.stepdirt_4",
|
||||
"voxygen.audio.sfx.footsteps.stepdirt_5",
|
||||
"voxygen.audio.sfx.footsteps.stepdirt_6",
|
||||
"voxygen.audio.sfx.footsteps.stepdirt_7",
|
||||
"voxygen.audio.sfx.footsteps.stepdirt_8",
|
||||
],
|
||||
threshold: 0.25,
|
||||
),
|
||||
(
|
||||
trigger: GliderOpen,
|
||||
files: [
|
||||
"voxygen.audio.sfx.glider_open",
|
||||
],
|
||||
threshold: 0.5,
|
||||
),
|
||||
(
|
||||
trigger: GliderClose,
|
||||
files: [
|
||||
"voxygen.audio.sfx.glider_close",
|
||||
],
|
||||
threshold: 0.5,
|
||||
),
|
||||
(
|
||||
trigger: Attack(Sword),
|
||||
files: [
|
||||
"voxygen.audio.sfx.weapon.sword",
|
||||
],
|
||||
threshold: 0.5,
|
||||
),
|
||||
(
|
||||
trigger: Attack(Hammer),
|
||||
files: [
|
||||
"voxygen.audio.sfx.weapon.sword",
|
||||
],
|
||||
threshold: 0.5,
|
||||
),
|
||||
(
|
||||
trigger: Attack(Bow),
|
||||
files: [
|
||||
"voxygen.audio.sfx.weapon.bow",
|
||||
],
|
||||
threshold: 0.5,
|
||||
),
|
||||
(
|
||||
trigger: LevelUp,
|
||||
files: [
|
||||
"voxygen.audio.sfx.chat_message_received"
|
||||
],
|
||||
threshold: 0.2,
|
||||
),
|
||||
(
|
||||
trigger: InventoryAdd,
|
||||
files: [
|
||||
"voxygen.audio.sfx.inventory_add"
|
||||
],
|
||||
threshold: 0.2,
|
||||
),
|
||||
],
|
||||
)
|
BIN
assets/voxygen/audio/sfx/blank.wav
Normal file
BIN
assets/voxygen/audio/sfx/blank.wav
Normal file
Binary file not shown.
BIN
assets/voxygen/audio/sfx/chat_message_received.wav
Normal file
BIN
assets/voxygen/audio/sfx/chat_message_received.wav
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
assets/voxygen/audio/sfx/glider_close.wav
Normal file
BIN
assets/voxygen/audio/sfx/glider_close.wav
Normal file
Binary file not shown.
BIN
assets/voxygen/audio/sfx/glider_open.wav
Normal file
BIN
assets/voxygen/audio/sfx/glider_open.wav
Normal file
Binary file not shown.
BIN
assets/voxygen/audio/sfx/inventory_add.wav
Normal file
BIN
assets/voxygen/audio/sfx/inventory_add.wav
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
assets/voxygen/audio/sfx/weapon/bow.wav
Normal file
BIN
assets/voxygen/audio/sfx/weapon/bow.wav
Normal file
Binary file not shown.
BIN
assets/voxygen/audio/sfx/weapon/sword.wav
Normal file
BIN
assets/voxygen/audio/sfx/weapon/sword.wav
Normal file
Binary file not shown.
@ -1,10 +1,53 @@
|
||||
use crate::comp;
|
||||
use comp::item::Tool;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use specs::Entity as EcsEntity;
|
||||
use sphynx::Uid;
|
||||
use std::{collections::VecDeque, ops::DerefMut};
|
||||
use vek::*;
|
||||
|
||||
pub struct SfxEventItem {
|
||||
pub sfx: SfxEvent,
|
||||
pub pos: Option<Vec3<f32>>,
|
||||
}
|
||||
|
||||
impl SfxEventItem {
|
||||
pub fn new(sfx: SfxEvent, pos: Option<Vec3<f32>>) -> Self {
|
||||
Self { sfx, pos }
|
||||
}
|
||||
|
||||
pub fn at_player_position(sfx: SfxEvent) -> Self {
|
||||
Self { sfx, pos: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
|
||||
pub enum SfxEvent {
|
||||
Idle,
|
||||
PlaceBlock,
|
||||
RemoveBlock,
|
||||
OpenChest,
|
||||
ChatMessageReceived,
|
||||
OpenBag,
|
||||
LevelUp,
|
||||
Roll,
|
||||
Climb,
|
||||
Swim,
|
||||
Run,
|
||||
GliderOpen,
|
||||
Glide,
|
||||
GliderClose,
|
||||
Jump,
|
||||
Fall,
|
||||
InventoryAdd,
|
||||
InventoryDrop,
|
||||
LightLantern,
|
||||
ExtinguishLantern,
|
||||
Attack(Tool),
|
||||
AttackWolf,
|
||||
}
|
||||
|
||||
pub enum LocalEvent {
|
||||
Jump(EcsEntity),
|
||||
WallLeap {
|
||||
|
@ -5,7 +5,7 @@ use serde_json;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum NpcKind {
|
||||
Humanoid,
|
||||
Wolf,
|
||||
|
@ -3,7 +3,7 @@ pub use sphynx::Uid;
|
||||
|
||||
use crate::{
|
||||
comp,
|
||||
event::{EventBus, LocalEvent, ServerEvent},
|
||||
event::{EventBus, LocalEvent, ServerEvent, SfxEventItem},
|
||||
msg::{EcsCompPacket, EcsResPacket},
|
||||
region::RegionMap,
|
||||
sys,
|
||||
@ -173,6 +173,7 @@ impl State {
|
||||
ecs.add_resource(TerrainChanges::default());
|
||||
ecs.add_resource(EventBus::<ServerEvent>::default());
|
||||
ecs.add_resource(EventBus::<LocalEvent>::default());
|
||||
ecs.add_resource(EventBus::<SfxEventItem>::default());
|
||||
ecs.add_resource(RegionMap::new());
|
||||
}
|
||||
|
||||
|
@ -1,26 +1,31 @@
|
||||
use crate::{
|
||||
comp::{HealthSource, Stats},
|
||||
event::{EventBus, ServerEvent},
|
||||
event::{EventBus, ServerEvent, SfxEvent, SfxEventItem},
|
||||
state::DeltaTime,
|
||||
};
|
||||
use specs::{Entities, Join, Read, System, WriteStorage};
|
||||
|
||||
/// This system kills players
|
||||
/// and handles players levelling up
|
||||
pub struct Sys;
|
||||
impl<'a> System<'a> for Sys {
|
||||
type SystemData = (
|
||||
Entities<'a>,
|
||||
Read<'a, DeltaTime>,
|
||||
Read<'a, EventBus<ServerEvent>>,
|
||||
Read<'a, EventBus<SfxEventItem>>,
|
||||
WriteStorage<'a, Stats>,
|
||||
);
|
||||
|
||||
fn run(&mut self, (entities, dt, event_bus, mut stats): Self::SystemData) {
|
||||
let mut event_emitter = event_bus.emitter();
|
||||
fn run(
|
||||
&mut self,
|
||||
(entities, dt, server_event_bus, audio_event_bus, mut stats): Self::SystemData,
|
||||
) {
|
||||
let mut server_event_emitter = server_event_bus.emitter();
|
||||
|
||||
for (entity, mut stat) in (&entities, &mut stats).join() {
|
||||
if stat.should_die() && !stat.is_dead {
|
||||
event_emitter.emit(ServerEvent::Destroy {
|
||||
server_event_emitter.emit(ServerEvent::Destroy {
|
||||
entity,
|
||||
cause: stat.health.last_change.1.cause,
|
||||
});
|
||||
@ -36,6 +41,11 @@ impl<'a> System<'a> for Sys {
|
||||
stat.exp.change_maximum_by(25);
|
||||
stat.level.change_by(1);
|
||||
}
|
||||
|
||||
audio_event_bus
|
||||
.emitter()
|
||||
.emit(SfxEventItem::at_player_position(SfxEvent::LevelUp));
|
||||
|
||||
stat.update_max_hp();
|
||||
stat.health
|
||||
.set_to(stat.health.maximum(), HealthSource::LevelUp)
|
||||
|
@ -1,6 +1,8 @@
|
||||
pub mod channel;
|
||||
pub mod fader;
|
||||
pub mod sfx;
|
||||
pub mod soundcache;
|
||||
|
||||
use channel::{AudioType, Channel};
|
||||
use fader::Fader;
|
||||
use soundcache::SoundCache;
|
||||
|
443
voxygen/src/audio/sfx/event_mapper.rs
Normal file
443
voxygen/src/audio/sfx/event_mapper.rs
Normal file
@ -0,0 +1,443 @@
|
||||
/// sfx::event_mapper watches the local entities and determines which sfx to emit,
|
||||
/// and the position at which the sound should be emitted from
|
||||
use crate::audio::sfx::{SfxTriggerItem, SfxTriggers};
|
||||
|
||||
use client::Client;
|
||||
use common::{
|
||||
comp::{ActionState, Body, CharacterState, ItemKind, MovementState, Pos, Stats, Vel},
|
||||
event::{EventBus, SfxEvent, SfxEventItem},
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use specs::{Entity as EcsEntity, Join};
|
||||
use std::time::{Duration, Instant};
|
||||
use vek::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LastSfxEvent {
|
||||
event: SfxEvent,
|
||||
time: Instant,
|
||||
}
|
||||
|
||||
pub struct SfxEventMapper {
|
||||
event_history: HashMap<EcsEntity, LastSfxEvent>,
|
||||
}
|
||||
|
||||
impl SfxEventMapper {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
event_history: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maintain(&mut self, client: &Client, triggers: &SfxTriggers) {
|
||||
const SFX_DIST_LIMIT_SQR: f32 = 22500.0;
|
||||
let ecs = client.state().ecs();
|
||||
|
||||
let player_position = ecs
|
||||
.read_storage::<Pos>()
|
||||
.get(client.entity())
|
||||
.map_or(Vec3::zero(), |pos| pos.0);
|
||||
|
||||
for (entity, pos, body, vel, stats, character) in (
|
||||
&ecs.entities(),
|
||||
&ecs.read_storage::<Pos>(),
|
||||
&ecs.read_storage::<Body>(),
|
||||
&ecs.read_storage::<Vel>(),
|
||||
&ecs.read_storage::<Stats>(),
|
||||
ecs.read_storage::<CharacterState>().maybe(),
|
||||
)
|
||||
.join()
|
||||
.filter(|(_, e_pos, ..)| {
|
||||
(e_pos.0.distance_squared(player_position)) < SFX_DIST_LIMIT_SQR
|
||||
})
|
||||
{
|
||||
if let (pos, body, Some(character), stats, vel) = (pos, body, character, stats, vel) {
|
||||
let state = self
|
||||
.event_history
|
||||
.entry(entity)
|
||||
.or_insert_with(|| LastSfxEvent {
|
||||
event: SfxEvent::Idle,
|
||||
time: Instant::now(),
|
||||
});
|
||||
|
||||
let mapped_event = match body {
|
||||
Body::Humanoid(_) => {
|
||||
Self::map_character_event(character, state.event.clone(), vel.0, stats)
|
||||
}
|
||||
Body::QuadrupedMedium(_) => {
|
||||
Self::map_quadriped_event(character, state.event.clone(), vel.0, stats)
|
||||
}
|
||||
_ => SfxEvent::Idle,
|
||||
};
|
||||
|
||||
// Check for SFX config entry for this movement
|
||||
let sfx_trigger_item: Option<&SfxTriggerItem> = triggers
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| item.trigger == mapped_event);
|
||||
|
||||
if Self::should_emit(state, sfx_trigger_item) {
|
||||
ecs.read_resource::<EventBus<SfxEventItem>>()
|
||||
.emitter()
|
||||
.emit(SfxEventItem::new(mapped_event, Some(pos.0)));
|
||||
|
||||
// Update the last play time
|
||||
state.event = mapped_event;
|
||||
state.time = Instant::now();
|
||||
} else {
|
||||
// Keep the last event, it may not have an SFX trigger but it helps us determine the next one
|
||||
state.event = mapped_event;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.cleanup(client.entity());
|
||||
}
|
||||
|
||||
/// As the player explores the world, we track the last event of the nearby entities to determine the correct
|
||||
/// SFX item to play next based on their activity. `cleanup` will remove entities from event tracking if they
|
||||
/// have not triggered an event for > n seconds. This prevents stale records from bloating the Map size.
|
||||
fn cleanup(&mut self, player: EcsEntity) {
|
||||
const TRACKING_TIMEOUT: u64 = 15;
|
||||
|
||||
let now = Instant::now();
|
||||
self.event_history.retain(|entity, event| {
|
||||
now.duration_since(event.time) < Duration::from_secs(TRACKING_TIMEOUT)
|
||||
|| entity.id() == player.id()
|
||||
});
|
||||
}
|
||||
|
||||
/// When specific entity movements are detected, the associated sound (if any) needs to satisfy two conditions to
|
||||
/// be allowed to play:
|
||||
/// 1. An sfx.ron entry exists for the movement (we need to know which sound file(s) to play)
|
||||
/// 2. The sfx has not been played since it's timeout threshold has elapsed, which prevents firing every tick
|
||||
fn should_emit(
|
||||
last_play_entry: &LastSfxEvent,
|
||||
sfx_trigger_item: Option<&SfxTriggerItem>,
|
||||
) -> bool {
|
||||
if let Some(item) = sfx_trigger_item {
|
||||
if last_play_entry.event == item.trigger {
|
||||
last_play_entry.time.elapsed().as_secs_f64() >= item.threshold
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Voxygen has an existing list of character states via `MovementState::*` and `ActivityState::*`
|
||||
/// however that list does not provide enough resolution to target specific entity events, such
|
||||
/// as opening or closing the glider. These methods translate those entity states with some additional
|
||||
/// data into more specific `SfxEvent`'s which we attach sounds to
|
||||
fn map_quadriped_event(
|
||||
current_event: &CharacterState,
|
||||
previous_event: SfxEvent,
|
||||
vel: Vec3<f32>,
|
||||
stats: &Stats,
|
||||
) -> SfxEvent {
|
||||
match (
|
||||
current_event.movement,
|
||||
current_event.action,
|
||||
previous_event,
|
||||
vel,
|
||||
stats,
|
||||
) {
|
||||
(_, ActionState::Attack { .. }, _, _, stats) => match stats.name.as_ref() {
|
||||
"Wolf" => SfxEvent::AttackWolf,
|
||||
_ => SfxEvent::Idle,
|
||||
},
|
||||
_ => SfxEvent::Idle,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_character_event(
|
||||
current_event: &CharacterState,
|
||||
previous_event: SfxEvent,
|
||||
vel: Vec3<f32>,
|
||||
stats: &Stats,
|
||||
) -> SfxEvent {
|
||||
match (
|
||||
current_event.movement,
|
||||
current_event.action,
|
||||
previous_event,
|
||||
vel,
|
||||
stats,
|
||||
) {
|
||||
(MovementState::Roll { .. }, ..) => SfxEvent::Roll,
|
||||
(MovementState::Climb, ..) => SfxEvent::Climb,
|
||||
(MovementState::Swim, ..) => SfxEvent::Swim,
|
||||
(MovementState::Run, ..) => SfxEvent::Run,
|
||||
(MovementState::Jump, _, previous_event, vel, _) => {
|
||||
// MovementState::Jump only indicates !on_ground
|
||||
if previous_event != SfxEvent::Glide {
|
||||
if vel.z > 0.0 {
|
||||
SfxEvent::Jump
|
||||
} else {
|
||||
SfxEvent::Fall
|
||||
}
|
||||
} else {
|
||||
SfxEvent::GliderClose
|
||||
}
|
||||
}
|
||||
(MovementState::Glide, _, previous_event, ..) => {
|
||||
if previous_event != SfxEvent::GliderOpen && previous_event != SfxEvent::Glide {
|
||||
SfxEvent::GliderOpen
|
||||
} else {
|
||||
SfxEvent::Glide
|
||||
}
|
||||
}
|
||||
(_, ActionState::Attack { .. }, _, _, stats) => {
|
||||
match &stats.equipment.main.as_ref().map(|i| &i.kind) {
|
||||
Some(ItemKind::Tool { kind, .. }) => SfxEvent::Attack(*kind),
|
||||
_ => SfxEvent::Idle,
|
||||
}
|
||||
}
|
||||
_ => SfxEvent::Idle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use common::{
|
||||
assets,
|
||||
comp::{item::Tool, ActionState, MovementState, Stats},
|
||||
event::SfxEvent,
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[test]
|
||||
fn no_item_config_no_emit() {
|
||||
let last_sfx_event = LastSfxEvent {
|
||||
event: SfxEvent::Idle,
|
||||
time: Instant::now(),
|
||||
};
|
||||
|
||||
let result = SfxEventMapper::should_emit(&last_sfx_event, None);
|
||||
|
||||
assert_eq!(result, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_but_played_since_threshold_no_emit() {
|
||||
let trigger_item = SfxTriggerItem {
|
||||
trigger: SfxEvent::Run,
|
||||
files: vec![String::from("some.path.to.sfx.file")],
|
||||
threshold: 1.0,
|
||||
};
|
||||
|
||||
// Triggered a 'Run' 0 seconds ago
|
||||
let last_sfx_event = LastSfxEvent {
|
||||
event: SfxEvent::Run,
|
||||
time: Instant::now(),
|
||||
};
|
||||
|
||||
let result = SfxEventMapper::should_emit(&last_sfx_event, Some(&trigger_item));
|
||||
|
||||
assert_eq!(result, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_and_not_played_since_threshold_emits() {
|
||||
let trigger_item = SfxTriggerItem {
|
||||
trigger: SfxEvent::Run,
|
||||
files: vec![String::from("some.path.to.sfx.file")],
|
||||
threshold: 0.5,
|
||||
};
|
||||
|
||||
let last_sfx_event = LastSfxEvent {
|
||||
event: SfxEvent::Idle,
|
||||
time: Instant::now().checked_add(Duration::from_secs(1)).unwrap(),
|
||||
};
|
||||
|
||||
let result = SfxEventMapper::should_emit(&last_sfx_event, Some(&trigger_item));
|
||||
|
||||
assert_eq!(result, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_previous_event_elapsed_emits() {
|
||||
let trigger_item = SfxTriggerItem {
|
||||
trigger: SfxEvent::Run,
|
||||
files: vec![String::from("some.path.to.sfx.file")],
|
||||
threshold: 0.5,
|
||||
};
|
||||
|
||||
let last_sfx_event = LastSfxEvent {
|
||||
event: SfxEvent::Run,
|
||||
time: Instant::now()
|
||||
.checked_sub(Duration::from_millis(500))
|
||||
.unwrap(),
|
||||
};
|
||||
|
||||
let result = SfxEventMapper::should_emit(&last_sfx_event, Some(&trigger_item));
|
||||
|
||||
assert_eq!(result, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_idle() {
|
||||
let stats = Stats::new(String::from("Test"), None);
|
||||
|
||||
let result = SfxEventMapper::map_character_event(
|
||||
&CharacterState {
|
||||
movement: MovementState::Stand,
|
||||
action: ActionState::Idle,
|
||||
},
|
||||
SfxEvent::Idle,
|
||||
Vec3::zero(),
|
||||
&stats,
|
||||
);
|
||||
|
||||
assert_eq!(result, SfxEvent::Idle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_run() {
|
||||
let stats = Stats::new(String::from("Test"), None);
|
||||
|
||||
let result = SfxEventMapper::map_character_event(
|
||||
&CharacterState {
|
||||
movement: MovementState::Run,
|
||||
action: ActionState::Idle,
|
||||
},
|
||||
SfxEvent::Idle,
|
||||
Vec3::zero(),
|
||||
&stats,
|
||||
);
|
||||
|
||||
assert_eq!(result, SfxEvent::Run);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_roll() {
|
||||
let stats = Stats::new(String::from("Test"), None);
|
||||
|
||||
let result = SfxEventMapper::map_character_event(
|
||||
&CharacterState {
|
||||
movement: MovementState::Roll {
|
||||
time_left: Duration::new(1, 0),
|
||||
},
|
||||
action: ActionState::Idle,
|
||||
},
|
||||
SfxEvent::Run,
|
||||
Vec3::zero(),
|
||||
&stats,
|
||||
);
|
||||
|
||||
assert_eq!(result, SfxEvent::Roll);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_jump_or_fall() {
|
||||
let stats = Stats::new(String::from("Test"), None);
|
||||
|
||||
// positive z velocity, the character is on the rise (jumping)
|
||||
let vel_jumping = Vec3::new(0.0, 0.0, 1.0);
|
||||
|
||||
let positive_result = SfxEventMapper::map_character_event(
|
||||
&CharacterState {
|
||||
movement: MovementState::Jump,
|
||||
action: ActionState::Idle,
|
||||
},
|
||||
SfxEvent::Idle,
|
||||
vel_jumping,
|
||||
&stats,
|
||||
);
|
||||
|
||||
assert_eq!(positive_result, SfxEvent::Jump);
|
||||
|
||||
// negative z velocity, the character is on the way down (!jumping)
|
||||
let vel_falling = Vec3::new(0.0, 0.0, -1.0);
|
||||
|
||||
let negative_result = SfxEventMapper::map_character_event(
|
||||
&CharacterState {
|
||||
movement: MovementState::Jump,
|
||||
action: ActionState::Idle,
|
||||
},
|
||||
SfxEvent::Idle,
|
||||
vel_falling,
|
||||
&stats,
|
||||
);
|
||||
|
||||
assert_eq!(negative_result, SfxEvent::Fall);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_glider_open() {
|
||||
let stats = Stats::new(String::from("Test"), None);
|
||||
|
||||
let result = SfxEventMapper::map_character_event(
|
||||
&CharacterState {
|
||||
movement: MovementState::Glide,
|
||||
action: ActionState::Idle,
|
||||
},
|
||||
SfxEvent::Jump,
|
||||
Vec3::zero(),
|
||||
&stats,
|
||||
);
|
||||
|
||||
assert_eq!(result, SfxEvent::GliderOpen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_glide() {
|
||||
let stats = Stats::new(String::from("Test"), None);
|
||||
|
||||
let result = SfxEventMapper::map_character_event(
|
||||
&CharacterState {
|
||||
movement: MovementState::Glide,
|
||||
action: ActionState::Idle,
|
||||
},
|
||||
SfxEvent::Glide,
|
||||
Vec3::zero(),
|
||||
&stats,
|
||||
);
|
||||
|
||||
assert_eq!(result, SfxEvent::Glide);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_glider_close() {
|
||||
let stats = Stats::new(String::from("Test"), None);
|
||||
|
||||
let result = SfxEventMapper::map_character_event(
|
||||
&CharacterState {
|
||||
movement: MovementState::Jump,
|
||||
action: ActionState::Idle,
|
||||
},
|
||||
SfxEvent::Glide,
|
||||
Vec3::zero(),
|
||||
&stats,
|
||||
);
|
||||
|
||||
assert_eq!(result, SfxEvent::GliderClose);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_attack() {
|
||||
let stats = Stats::new(
|
||||
String::from("Test"),
|
||||
Some(assets::load_expect_cloned(
|
||||
"common.items.weapons.starter_sword",
|
||||
)),
|
||||
);
|
||||
|
||||
let result = SfxEventMapper::map_character_event(
|
||||
&CharacterState {
|
||||
movement: MovementState::Stand,
|
||||
action: ActionState::Attack {
|
||||
time_left: Duration::new(1, 0),
|
||||
applied: true,
|
||||
},
|
||||
},
|
||||
SfxEvent::Idle,
|
||||
Vec3::zero(),
|
||||
&stats,
|
||||
);
|
||||
|
||||
assert_eq!(result, SfxEvent::Attack(Tool::Sword));
|
||||
}
|
||||
}
|
95
voxygen/src/audio/sfx/mod.rs
Normal file
95
voxygen/src/audio/sfx/mod.rs
Normal file
@ -0,0 +1,95 @@
|
||||
/// The SfxManager listens for SFX events and plays the sound at the provided position
|
||||
mod event_mapper;
|
||||
|
||||
use crate::audio::AudioFrontend;
|
||||
use client::Client;
|
||||
use common::{
|
||||
assets,
|
||||
comp::{Ori, Pos},
|
||||
event::{EventBus, SfxEvent, SfxEventItem},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use vek::*;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SfxTriggerItem {
|
||||
pub trigger: SfxEvent,
|
||||
pub files: Vec<String>,
|
||||
pub threshold: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SfxTriggers {
|
||||
pub items: Vec<SfxTriggerItem>,
|
||||
}
|
||||
|
||||
pub struct SfxMgr {
|
||||
triggers: SfxTriggers,
|
||||
event_mapper: event_mapper::SfxEventMapper,
|
||||
}
|
||||
|
||||
impl SfxMgr {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
triggers: Self::load_sfx_items(),
|
||||
event_mapper: event_mapper::SfxEventMapper::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maintain(&mut self, audio: &mut AudioFrontend, client: &Client) {
|
||||
self.event_mapper.maintain(client, &self.triggers);
|
||||
let ecs = client.state().ecs();
|
||||
|
||||
let player_position = ecs
|
||||
.read_storage::<Pos>()
|
||||
.get(client.entity())
|
||||
.map_or(Vec3::zero(), |pos| pos.0);
|
||||
|
||||
let player_ori = ecs
|
||||
.read_storage::<Ori>()
|
||||
.get(client.entity())
|
||||
.map_or(Vec3::zero(), |pos| pos.0);
|
||||
|
||||
audio.set_listener_pos(&player_position, &player_ori);
|
||||
|
||||
let events = ecs.read_resource::<EventBus<SfxEventItem>>().recv_all();
|
||||
|
||||
for event in events {
|
||||
let position = match event.pos {
|
||||
Some(pos) => pos,
|
||||
_ => player_position,
|
||||
};
|
||||
|
||||
// Get the SFX config entry for this movement
|
||||
let sfx_trigger_item: Option<&SfxTriggerItem> = self
|
||||
.triggers
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| item.trigger == event.sfx);
|
||||
|
||||
if sfx_trigger_item.is_some() {
|
||||
let item = sfx_trigger_item.expect("Invalid sfx item");
|
||||
|
||||
let sfx_file = match item.files.len() {
|
||||
1 => item
|
||||
.files
|
||||
.last()
|
||||
.expect("Failed to determine sound file for this trigger item."),
|
||||
_ => {
|
||||
let rand_step = rand::random::<usize>() % item.files.len();
|
||||
&item.files[rand_step]
|
||||
}
|
||||
};
|
||||
|
||||
audio.play_sound(sfx_file, position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_sfx_items() -> SfxTriggers {
|
||||
let file = assets::load_file("voxygen.audio.sfx", &["ron"])
|
||||
.expect("Failed to load the sfx config file");
|
||||
|
||||
ron::de::from_reader(file).expect("Error parsing sfx manifest")
|
||||
}
|
||||
}
|
@ -1,16 +1,14 @@
|
||||
pub mod camera;
|
||||
pub mod figure;
|
||||
pub mod sound;
|
||||
pub mod terrain;
|
||||
|
||||
use self::{
|
||||
camera::{Camera, CameraMode},
|
||||
figure::FigureMgr,
|
||||
sound::SoundMgr,
|
||||
terrain::Terrain,
|
||||
};
|
||||
use crate::{
|
||||
audio::AudioFrontend,
|
||||
audio::{sfx::SfxMgr, AudioFrontend},
|
||||
render::{
|
||||
create_pp_mesh, create_skybox_mesh, Consts, Globals, Light, Model, PostProcessLocals,
|
||||
PostProcessPipeline, Renderer, Shadow, SkyboxLocals, SkyboxPipeline,
|
||||
@ -58,7 +56,7 @@ pub struct Scene {
|
||||
select_pos: Option<Vec3<i32>>,
|
||||
|
||||
figure_mgr: FigureMgr,
|
||||
sound_mgr: SoundMgr,
|
||||
sfx_mgr: SfxMgr,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
@ -91,7 +89,7 @@ impl Scene {
|
||||
select_pos: None,
|
||||
|
||||
figure_mgr: FigureMgr::new(),
|
||||
sound_mgr: SoundMgr::new(),
|
||||
sfx_mgr: SfxMgr::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,8 +285,8 @@ impl Scene {
|
||||
// Remove unused figures.
|
||||
self.figure_mgr.clean(client.get_tick());
|
||||
|
||||
// Maintain audio
|
||||
self.sound_mgr.maintain(audio, client);
|
||||
// Maintain sfx
|
||||
self.sfx_mgr.maintain(audio, client);
|
||||
}
|
||||
|
||||
/// Render the scene using the provided `Renderer`.
|
||||
|
@ -1,69 +0,0 @@
|
||||
use crate::audio::AudioFrontend;
|
||||
use client::Client;
|
||||
use common::comp::{Body, CharacterState, MovementState::*, Ori, Pos};
|
||||
use hashbrown::HashMap;
|
||||
use specs::{Entity as EcsEntity, Join};
|
||||
use std::time::Instant;
|
||||
use vek::*;
|
||||
|
||||
pub struct AnimState {
|
||||
last_step_sound: Instant,
|
||||
}
|
||||
|
||||
pub struct SoundMgr {
|
||||
character_states: HashMap<EcsEntity, AnimState>,
|
||||
}
|
||||
|
||||
impl SoundMgr {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
character_states: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maintain(&mut self, audio: &mut AudioFrontend, client: &Client) {
|
||||
const SFX_DIST_LIMIT_SQR: f32 = 22500.0;
|
||||
let ecs = client.state().ecs();
|
||||
// Get player position.
|
||||
let player_pos = ecs
|
||||
.read_storage::<Pos>()
|
||||
.get(client.entity())
|
||||
.map_or(Vec3::zero(), |pos| pos.0);
|
||||
|
||||
let player_ori = ecs
|
||||
.read_storage::<Ori>()
|
||||
.get(client.entity())
|
||||
.map_or(Vec3::zero(), |pos| pos.0);
|
||||
|
||||
audio.set_listener_pos(&player_pos, &player_ori);
|
||||
|
||||
for (entity, pos, body, character) in (
|
||||
&ecs.entities(),
|
||||
&ecs.read_storage::<Pos>(),
|
||||
&ecs.read_storage::<Body>(),
|
||||
ecs.read_storage::<CharacterState>().maybe(),
|
||||
)
|
||||
.join()
|
||||
.filter(|(_, e_pos, _, _)| (e_pos.0.distance_squared(player_pos)) < SFX_DIST_LIMIT_SQR)
|
||||
{
|
||||
if let (Body::Humanoid(_), Some(character)) = (body, character) {
|
||||
let state = self
|
||||
.character_states
|
||||
.entry(entity)
|
||||
.or_insert_with(|| AnimState {
|
||||
last_step_sound: Instant::now(),
|
||||
});
|
||||
|
||||
if character.movement == Run && state.last_step_sound.elapsed().as_secs_f64() > 0.25
|
||||
{
|
||||
let rand_step = (rand::random::<usize>() % 5) + 1;
|
||||
audio.play_sound(
|
||||
&format!("voxygen.audio.sfx.steps.step_{}", rand_step),
|
||||
pos.0,
|
||||
);
|
||||
state.last_step_sound = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user