veloren_voxygen/audio/sfx/event_mapper/movement/
mod.rs

1/// EventMapper::Movement watches the movement states of surrounding entities,
2/// and triggers sfx related to running, climbing and gliding, at a volume
3/// proportionate to the extity's size
4use super::EventMapper;
5use crate::{
6    AudioFrontend,
7    audio::sfx::{SFX_DIST_LIMIT_SQR, SfxEvent, SfxTriggerItem, SfxTriggers},
8    scene::{Camera, Terrain},
9};
10use client::Client;
11use common::{
12    comp::{Body, CharacterState, PhysicsState, Pos, Scale, Vel},
13    resources::DeltaTime,
14    states,
15    terrain::{BlockKind, TerrainChunk},
16};
17use common_state::State;
18use hashbrown::HashMap;
19use rand::prelude::*;
20use specs::{Entity as EcsEntity, Join, LendJoin, WorldExt};
21use std::time::{Duration, Instant};
22use vek::*;
23
24#[derive(Clone)]
25struct PreviousEntityState {
26    event: SfxEvent,
27    time: Instant,
28    on_ground: bool,
29    in_water: bool,
30    steps_taken: f32,
31}
32
33impl Default for PreviousEntityState {
34    fn default() -> Self {
35        Self {
36            event: SfxEvent::Idle,
37            time: Instant::now(),
38            on_ground: true,
39            in_water: false,
40            steps_taken: 0.0,
41        }
42    }
43}
44
45pub struct MovementEventMapper {
46    event_history: HashMap<EcsEntity, PreviousEntityState>,
47}
48
49impl EventMapper for MovementEventMapper {
50    fn maintain(
51        &mut self,
52        audio: &mut AudioFrontend,
53        state: &State,
54        player_entity: specs::Entity,
55        camera: &Camera,
56        triggers: &SfxTriggers,
57        _terrain: &Terrain<TerrainChunk>,
58        _client: &Client,
59    ) {
60        let ecs = state.ecs();
61
62        let cam_pos = camera.get_pos_with_focus();
63
64        for (entity, pos, vel, body, scale, physics, character) in (
65            &ecs.entities(),
66            &ecs.read_storage::<Pos>(),
67            &ecs.read_storage::<Vel>(),
68            &ecs.read_storage::<Body>(),
69            ecs.read_storage::<Scale>().maybe(),
70            &ecs.read_storage::<PhysicsState>(),
71            ecs.read_storage::<CharacterState>().maybe(),
72        )
73            .join()
74            .filter(|(_, e_pos, ..)| (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR)
75        {
76            if let Some(character) = character {
77                let internal_state = self.event_history.entry(entity).or_default();
78
79                // Get the underfoot block
80                let block_position = Vec3::new(pos.0.x, pos.0.y, pos.0.z - 1.0).map(|x| x as i32);
81                let underfoot_block_kind = match state.get_block(block_position) {
82                    Some(block) => block.kind(),
83                    None => BlockKind::Air,
84                };
85
86                let mapped_event = match body {
87                    Body::Humanoid(_) => Self::map_movement_event(
88                        character,
89                        physics,
90                        internal_state,
91                        vel.0,
92                        underfoot_block_kind,
93                    ),
94                    Body::QuadrupedMedium(_) | Body::QuadrupedSmall(_) | Body::QuadrupedLow(_) => {
95                        Self::map_quadruped_movement_event(physics, vel.0, underfoot_block_kind)
96                    },
97                    Body::BirdMedium(_) | Body::BirdLarge(_) | Body::BipedLarge(_) => {
98                        Self::map_non_humanoid_movement_event(physics, vel.0, underfoot_block_kind)
99                    },
100                    _ => SfxEvent::Idle, // Ignore fish, etc...
101                };
102
103                // Check for SFX config entry for this movement
104                if Self::should_emit(internal_state, triggers.get_key_value(&mapped_event))
105                    && let Some(player_pos) = state.read_component_copied::<Pos>(player_entity)
106                {
107                    let sfx_trigger_item = triggers.get_key_value(&mapped_event);
108                    audio.emit_sfx(
109                        sfx_trigger_item,
110                        pos.0,
111                        Some(Self::get_volume_for_body_type(body)),
112                        player_pos.0,
113                    );
114                    internal_state.time = Instant::now();
115                    internal_state.steps_taken = 0.0;
116                }
117
118                // update state to determine the next event. We only record the time (above) if
119                // it was dispatched
120                internal_state.event = mapped_event;
121                internal_state.on_ground = physics.on_ground.is_some();
122                internal_state.in_water = physics.in_liquid().is_some();
123                let dt = ecs.fetch::<DeltaTime>().0;
124                internal_state.steps_taken +=
125                    vel.0.magnitude() * dt / (body.stride_length() * scale.map_or(1.0, |s| s.0));
126            }
127        }
128
129        self.cleanup(player_entity);
130    }
131}
132
133impl MovementEventMapper {
134    pub fn new() -> Self {
135        Self {
136            event_history: HashMap::new(),
137        }
138    }
139
140    /// As the player explores the world, we track the last event of the nearby
141    /// entities to determine the correct SFX item to play next based on
142    /// their activity. `cleanup` will remove entities from event tracking if
143    /// they have not triggered an event for > n seconds. This prevents
144    /// stale records from bloating the Map size.
145    fn cleanup(&mut self, player: EcsEntity) {
146        const TRACKING_TIMEOUT: u64 = 10;
147
148        let now = Instant::now();
149        self.event_history.retain(|entity, event| {
150            now.duration_since(event.time) < Duration::from_secs(TRACKING_TIMEOUT)
151                || entity.id() == player.id()
152        });
153    }
154
155    /// When specific entity movements are detected, the associated sound (if
156    /// any) needs to satisfy two conditions to be allowed to play:
157    /// 1. An sfx.ron entry exists for the movement (we need to know which sound
158    ///    file(s) to play)
159    /// 2. The sfx has not been played since it's timeout threshold has elapsed,
160    ///    which prevents firing every tick. For movement, threshold is not a
161    ///    time, but a distance.
162    fn should_emit(
163        previous_state: &PreviousEntityState,
164        sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
165    ) -> bool {
166        if let Some((event, item)) = sfx_trigger_item {
167            if &previous_state.event == event {
168                match event {
169                    SfxEvent::Run(_) => previous_state.steps_taken >= item.threshold,
170                    SfxEvent::Climb => previous_state.steps_taken >= item.threshold,
171                    SfxEvent::QuadRun(_) => previous_state.steps_taken >= item.threshold,
172                    _ => previous_state.time.elapsed().as_secs_f32() >= item.threshold,
173                }
174            } else {
175                true
176            }
177        } else {
178            false
179        }
180    }
181
182    /// Voxygen has an existing list of character states; however that list does
183    /// not provide enough resolution to target specific entity events, such
184    /// as opening or closing the glider. These methods translate those
185    /// entity states with some additional data into more specific
186    /// `SfxEvent`'s which we attach sounds to
187    fn map_movement_event(
188        character_state: &CharacterState,
189        physics_state: &PhysicsState,
190        previous_state: &PreviousEntityState,
191        vel: Vec3<f32>,
192        underfoot_block_kind: BlockKind,
193    ) -> SfxEvent {
194        // Match run / roll / swim state
195        if physics_state.in_liquid().is_some() && vel.magnitude() > 2.0
196            || !previous_state.in_water && physics_state.in_liquid().is_some()
197        {
198            return SfxEvent::Swim;
199        } else if physics_state.on_ground.is_some() && vel.magnitude() > 0.1
200            || !previous_state.on_ground && physics_state.on_ground.is_some()
201        {
202            return if let CharacterState::Roll(data) = character_state {
203                if data.static_data.was_cancel {
204                    SfxEvent::RollCancel
205                } else {
206                    SfxEvent::Roll
207                }
208            } else if character_state.is_stealthy() {
209                SfxEvent::Sneak
210            } else {
211                match underfoot_block_kind {
212                    BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::Run(BlockKind::Snow),
213                    BlockKind::Rock
214                    | BlockKind::WeakRock
215                    | BlockKind::GlowingRock
216                    | BlockKind::GlowingWeakRock
217                    | BlockKind::Ice => SfxEvent::Run(BlockKind::Rock),
218                    BlockKind::Earth => SfxEvent::Run(BlockKind::Earth),
219                    // BlockKind::Sand => SfxEvent::Run(BlockKind::Sand),
220                    BlockKind::Air => SfxEvent::Idle,
221                    _ => SfxEvent::Run(BlockKind::Grass),
222                }
223            };
224        }
225
226        // Match all other Movemement and Action states
227        match (previous_state.event.clone(), character_state) {
228            (_, CharacterState::Climb { .. }) => SfxEvent::Climb,
229            (_, CharacterState::Glide(glide))
230                if matches!(glide.booster, Some(states::glide::Boost::Forward(_))) =>
231            {
232                if thread_rng().gen_bool(0.5) {
233                    SfxEvent::FlameThrower
234                } else {
235                    SfxEvent::Idle
236                }
237            },
238            (_, CharacterState::Glide(_)) => SfxEvent::Glide,
239            _ => SfxEvent::Idle,
240        }
241    }
242
243    /// Maps a limited set of movements for other non-humanoid entities
244    fn map_non_humanoid_movement_event(
245        physics_state: &PhysicsState,
246        vel: Vec3<f32>,
247        underfoot_block_kind: BlockKind,
248    ) -> SfxEvent {
249        if physics_state.in_liquid().is_some() && vel.magnitude() > 2.0 {
250            SfxEvent::Swim
251        } else if physics_state.on_ground.is_some() && vel.magnitude() > 0.1 {
252            match underfoot_block_kind {
253                BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::Run(BlockKind::Snow),
254                BlockKind::Rock
255                | BlockKind::WeakRock
256                | BlockKind::GlowingRock
257                | BlockKind::GlowingWeakRock
258                | BlockKind::Ice => SfxEvent::Run(BlockKind::Rock),
259                // BlockKind::Sand => SfxEvent::Run(BlockKind::Sand),
260                BlockKind::Earth => SfxEvent::Run(BlockKind::Earth),
261                BlockKind::Air => SfxEvent::Idle,
262                _ => SfxEvent::Run(BlockKind::Grass),
263            }
264        } else {
265            SfxEvent::Idle
266        }
267    }
268
269    /// Maps a limited set of movements for quadruped entities
270    fn map_quadruped_movement_event(
271        physics_state: &PhysicsState,
272        vel: Vec3<f32>,
273        underfoot_block_kind: BlockKind,
274    ) -> SfxEvent {
275        if physics_state.in_liquid().is_some() && vel.magnitude() > 2.0 {
276            SfxEvent::Swim
277        } else if physics_state.on_ground.is_some() && vel.magnitude() > 0.1 {
278            match underfoot_block_kind {
279                BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::QuadRun(BlockKind::Snow),
280                BlockKind::Rock
281                | BlockKind::WeakRock
282                | BlockKind::GlowingRock
283                | BlockKind::GlowingWeakRock
284                | BlockKind::Ice => SfxEvent::QuadRun(BlockKind::Rock),
285                // BlockKind::Sand => SfxEvent::QuadRun(BlockKind::Sand),
286                BlockKind::Earth => SfxEvent::QuadRun(BlockKind::Earth),
287                BlockKind::Air => SfxEvent::Idle,
288                _ => SfxEvent::QuadRun(BlockKind::Grass),
289            }
290        } else {
291            SfxEvent::Idle
292        }
293    }
294
295    /// Returns a relative volume value for a body type. This helps us emit sfx
296    /// at a volume appropriate fot the entity we are emitting the event for
297    fn get_volume_for_body_type(body: &Body) -> f32 {
298        match body {
299            Body::Humanoid(_) => 0.9,
300            Body::QuadrupedSmall(_) => 0.3,
301            Body::QuadrupedMedium(_) => 0.7,
302            Body::QuadrupedLow(_) => 0.7,
303            Body::BirdMedium(_) => 0.3,
304            Body::BirdLarge(_) => 0.2,
305            Body::BipedLarge(_) => 1.0,
306            _ => 0.9,
307        }
308    }
309}
310
311#[cfg(test)] mod tests;