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 sfx_trigger_item = triggers.get_key_value(&mapped_event);
106                    audio.emit_sfx(
107                        sfx_trigger_item,
108                        pos.0,
109                        Some(Self::get_volume_for_body_type(body)),
110                    );
111                    internal_state.time = Instant::now();
112                    internal_state.steps_taken = 0.0;
113                }
114
115                // update state to determine the next event. We only record the time (above) if
116                // it was dispatched
117                internal_state.event = mapped_event;
118                internal_state.on_ground = physics.on_ground.is_some();
119                internal_state.in_water = physics.in_liquid().is_some();
120                let dt = ecs.fetch::<DeltaTime>().0;
121                internal_state.steps_taken +=
122                    vel.0.magnitude() * dt / (body.stride_length() * scale.map_or(1.0, |s| s.0));
123            }
124        }
125
126        self.cleanup(player_entity);
127    }
128}
129
130impl MovementEventMapper {
131    pub fn new() -> Self {
132        Self {
133            event_history: HashMap::new(),
134        }
135    }
136
137    /// As the player explores the world, we track the last event of the nearby
138    /// entities to determine the correct SFX item to play next based on
139    /// their activity. `cleanup` will remove entities from event tracking if
140    /// they have not triggered an event for > n seconds. This prevents
141    /// stale records from bloating the Map size.
142    fn cleanup(&mut self, player: EcsEntity) {
143        const TRACKING_TIMEOUT: u64 = 10;
144
145        let now = Instant::now();
146        self.event_history.retain(|entity, event| {
147            now.duration_since(event.time) < Duration::from_secs(TRACKING_TIMEOUT)
148                || entity.id() == player.id()
149        });
150    }
151
152    /// When specific entity movements are detected, the associated sound (if
153    /// any) needs to satisfy two conditions to be allowed to play:
154    /// 1. An sfx.ron entry exists for the movement (we need to know which sound
155    ///    file(s) to play)
156    /// 2. The sfx has not been played since it's timeout threshold has elapsed,
157    ///    which prevents firing every tick. For movement, threshold is not a
158    ///    time, but a distance.
159    fn should_emit(
160        previous_state: &PreviousEntityState,
161        sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
162    ) -> bool {
163        if let Some((event, item)) = sfx_trigger_item {
164            if &previous_state.event == event {
165                match event {
166                    SfxEvent::Run(_) => previous_state.steps_taken >= item.threshold,
167                    SfxEvent::Climb => previous_state.steps_taken >= item.threshold,
168                    SfxEvent::QuadRun(_) => previous_state.steps_taken >= item.threshold,
169                    _ => previous_state.time.elapsed().as_secs_f32() >= item.threshold,
170                }
171            } else {
172                true
173            }
174        } else {
175            false
176        }
177    }
178
179    /// Voxygen has an existing list of character states; however that list does
180    /// not provide enough resolution to target specific entity events, such
181    /// as opening or closing the glider. These methods translate those
182    /// entity states with some additional data into more specific
183    /// `SfxEvent`'s which we attach sounds to
184    fn map_movement_event(
185        character_state: &CharacterState,
186        physics_state: &PhysicsState,
187        previous_state: &PreviousEntityState,
188        vel: Vec3<f32>,
189        underfoot_block_kind: BlockKind,
190    ) -> SfxEvent {
191        // Match run / roll / swim state
192        if physics_state.in_liquid().is_some() && vel.magnitude() > 2.0
193            || !previous_state.in_water && physics_state.in_liquid().is_some()
194        {
195            return SfxEvent::Swim;
196        } else if physics_state.on_ground.is_some() && vel.magnitude() > 0.1
197            || !previous_state.on_ground && physics_state.on_ground.is_some()
198        {
199            return if let CharacterState::Roll(data) = character_state {
200                if data.static_data.was_cancel {
201                    SfxEvent::RollCancel
202                } else {
203                    SfxEvent::Roll
204                }
205            } else if character_state.is_stealthy() {
206                SfxEvent::Sneak
207            } else {
208                match underfoot_block_kind {
209                    BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::Run(BlockKind::Snow),
210                    BlockKind::Rock
211                    | BlockKind::WeakRock
212                    | BlockKind::GlowingRock
213                    | BlockKind::GlowingWeakRock
214                    | BlockKind::Ice => SfxEvent::Run(BlockKind::Rock),
215                    BlockKind::Earth => SfxEvent::Run(BlockKind::Earth),
216                    // BlockKind::Sand => SfxEvent::Run(BlockKind::Sand),
217                    BlockKind::Air => SfxEvent::Idle,
218                    _ => SfxEvent::Run(BlockKind::Grass),
219                }
220            };
221        }
222
223        // Match all other Movemement and Action states
224        match (previous_state.event.clone(), character_state) {
225            (_, CharacterState::Climb { .. }) => SfxEvent::Climb,
226            (_, CharacterState::Glide(glide))
227                if matches!(glide.booster, Some(states::glide::Boost::Forward(_))) =>
228            {
229                if thread_rng().gen_bool(0.5) {
230                    SfxEvent::FlameThrower
231                } else {
232                    SfxEvent::Idle
233                }
234            },
235            (_, CharacterState::Glide(_)) => SfxEvent::Glide,
236            _ => SfxEvent::Idle,
237        }
238    }
239
240    /// Maps a limited set of movements for other non-humanoid entities
241    fn map_non_humanoid_movement_event(
242        physics_state: &PhysicsState,
243        vel: Vec3<f32>,
244        underfoot_block_kind: BlockKind,
245    ) -> SfxEvent {
246        if physics_state.in_liquid().is_some() && vel.magnitude() > 2.0 {
247            SfxEvent::Swim
248        } else if physics_state.on_ground.is_some() && vel.magnitude() > 0.1 {
249            match underfoot_block_kind {
250                BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::Run(BlockKind::Snow),
251                BlockKind::Rock
252                | BlockKind::WeakRock
253                | BlockKind::GlowingRock
254                | BlockKind::GlowingWeakRock
255                | BlockKind::Ice => SfxEvent::Run(BlockKind::Rock),
256                // BlockKind::Sand => SfxEvent::Run(BlockKind::Sand),
257                BlockKind::Earth => SfxEvent::Run(BlockKind::Earth),
258                BlockKind::Air => SfxEvent::Idle,
259                _ => SfxEvent::Run(BlockKind::Grass),
260            }
261        } else {
262            SfxEvent::Idle
263        }
264    }
265
266    /// Maps a limited set of movements for quadruped entities
267    fn map_quadruped_movement_event(
268        physics_state: &PhysicsState,
269        vel: Vec3<f32>,
270        underfoot_block_kind: BlockKind,
271    ) -> SfxEvent {
272        if physics_state.in_liquid().is_some() && vel.magnitude() > 2.0 {
273            SfxEvent::Swim
274        } else if physics_state.on_ground.is_some() && vel.magnitude() > 0.1 {
275            match underfoot_block_kind {
276                BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::QuadRun(BlockKind::Snow),
277                BlockKind::Rock
278                | BlockKind::WeakRock
279                | BlockKind::GlowingRock
280                | BlockKind::GlowingWeakRock
281                | BlockKind::Ice => SfxEvent::QuadRun(BlockKind::Rock),
282                // BlockKind::Sand => SfxEvent::QuadRun(BlockKind::Sand),
283                BlockKind::Earth => SfxEvent::QuadRun(BlockKind::Earth),
284                BlockKind::Air => SfxEvent::Idle,
285                _ => SfxEvent::QuadRun(BlockKind::Grass),
286            }
287        } else {
288            SfxEvent::Idle
289        }
290    }
291
292    /// Returns a relative volume value for a body type. This helps us emit sfx
293    /// at a volume appropriate fot the entity we are emitting the event for
294    fn get_volume_for_body_type(body: &Body) -> f32 {
295        match body {
296            Body::Humanoid(_) => 0.9,
297            Body::QuadrupedSmall(_) => 0.3,
298            Body::QuadrupedMedium(_) => 0.7,
299            Body::QuadrupedLow(_) => 0.7,
300            Body::BirdMedium(_) => 0.3,
301            Body::BirdLarge(_) => 0.2,
302            Body::BipedLarge(_) => 1.0,
303            _ => 0.9,
304        }
305    }
306}
307
308#[cfg(test)] mod tests;