Skip to main content

audio_code/
shortcuts.rs

1//! Shortcuts on [`Context`] for the things almost every game does.
2
3use glam::Vec3;
4
5use crate::audio::Sound;
6use crate::engine::Context;
7use crate::input::Key;
8use crate::physics::{BodyId, PhysicsEvent};
9
10/// A sound that follows a physics body.
11#[derive(Debug)]
12pub(crate) struct Attached {
13    body: BodyId,
14    sound: Sound,
15    last: Vec3,
16}
17
18impl Context {
19    /// Speaks text, interrupting current speech. Same as `ctx.speech.speak(text)`.
20    pub fn speak(&mut self, text: impl AsRef<str>) {
21        self.speech.speak(text);
22    }
23
24    /// Plays a sound once. Same as `ctx.audio.play(name)`.
25    pub fn play(&mut self, name: &str) -> Sound {
26        self.audio.play(name)
27    }
28
29    /// Plays a sound once at a position. Same as `ctx.audio.play_at(name, position)`.
30    pub fn play_at(&mut self, name: &str, position: Vec3) -> Sound {
31        self.audio.play_at(name, position)
32    }
33
34    /// Whether a key went down this frame. Same as `ctx.input.pressed(key)`.
35    pub fn pressed(&self, key: Key) -> bool {
36        self.input.pressed(key)
37    }
38
39    /// Whether a key is held. Same as `ctx.input.held(key)`.
40    pub fn held(&self, key: Key) -> bool {
41        self.input.held(key)
42    }
43
44    /// Returns `true` the first time it is called with `name`, and `false` afterwards.
45    /// Handy for one-time setup in closure games and for "say this only once" hints.
46    ///
47    /// ```no_run
48    /// # use audio_code::prelude::*;
49    /// # fn f(ctx: &mut Context) {
50    /// if ctx.once("intro") {
51    ///     ctx.speak("Welcome! Press F1 for help.");
52    /// }
53    /// # }
54    /// ```
55    pub fn once(&mut self, name: &str) -> bool {
56        self.once.insert(name.to_string())
57    }
58
59    /// Makes the listener (the player's ears) follow a body every frame: its position, its
60    /// [`facing`](crate::physics::Body::facing) and its [`pitch`](crate::physics::Body::pitch). After this you never call
61    /// `ctx.audio.set_listener` yourself.
62    pub fn follow(&mut self, body: BodyId) {
63        self.follow = Some(body);
64        self.sync_world();
65    }
66
67    /// Stops following a body with the listener.
68    pub fn unfollow(&mut self) {
69        self.follow = None;
70    }
71
72    /// The body the listener follows, if any.
73    pub fn followed(&self) -> Option<BodyId> {
74        self.follow
75    }
76
77    /// Plays a sound once at a body; the sound moves with the body.
78    pub fn play_on(&mut self, body: BodyId, name: &str) -> Sound {
79        let sound = self.audio.play_at(name, self.physics.position(body));
80        self.attach(body, sound.clone());
81        sound
82    }
83
84    /// Plays a looping sound at a body. It moves with the body and stops automatically when
85    /// the body is removed. Perfect for enemies, engines, coins and beacons:
86    ///
87    /// ```no_run
88    /// # use audio_code::prelude::*;
89    /// # fn f(ctx: &mut Context) {
90    /// let enemy = ctx.physics.add(Body::kinematic(vec3(5.0, 5.0, 0.0)).with_tag("enemy"));
91    /// ctx.loop_on(enemy, "growl");
92    /// # }
93    /// ```
94    pub fn loop_on(&mut self, body: BodyId, name: &str) -> Sound {
95        let sound = self.audio.play_looped_at(name, self.physics.position(body));
96        self.attach(body, sound.clone());
97        sound
98    }
99
100    /// Makes any positioned sound follow a body, e.g. one made with the sound builder:
101    /// `let s = ctx.audio.sound("hum").volume(0.3).looped().at(pos).play(); ctx.attach(body, s);`
102    pub fn attach(&mut self, body: BodyId, sound: Sound) {
103        let position = self.physics.position(body);
104        sound.set_position(position);
105        self.attached.push(Attached { body, sound, last: position });
106    }
107
108    /// Stops every sound attached to a body.
109    pub fn stop_sounds_on(&mut self, body: BodyId) {
110        self.attached.retain(|a| {
111            if a.body == body {
112                a.sound.stop();
113                false
114            } else {
115                true
116            }
117        });
118    }
119
120    /// Drives a vehicle with the keyboard: Up or W is the gas pedal, Down or S brakes and,
121    /// once the vehicle stands still, reverses, Left / Right or A / D steer, Space is the
122    /// handbrake. Call it every frame. Does nothing for bodies without a vehicle. While a
123    /// menu is open all pedals are released.
124    ///
125    /// ```no_run
126    /// # use audio_code::prelude::*;
127    /// # fn f(ctx: &mut Context) {
128    /// let car = ctx.physics.add(Body::car(Vec3::ZERO));
129    /// ctx.follow(car);
130    /// // in update():
131    /// ctx.drive(car);
132    /// # }
133    /// ```
134    pub fn drive(&mut self, vehicle: BodyId) {
135        let input = &self.input;
136        let throttle = (input.held(Key::Up) || input.held(Key::W)) as u8 as f32;
137        let brake = (input.held(Key::Down) || input.held(Key::S)) as u8 as f32;
138        let right = (input.held(Key::Right) || input.held(Key::D)) as i32;
139        let left = (input.held(Key::Left) || input.held(Key::A)) as i32;
140        let handbrake = input.held(Key::Space);
141        if let Some(v) = self.physics.vehicle_mut(vehicle) {
142            v.throttle = throttle;
143            v.brake = brake;
144            v.steer = (right - left) as f32;
145            v.handbrake = handbrake;
146        }
147    }
148
149    /// Honks: plays the vehicle's horn sound at the vehicle. Returns a silent handle if it
150    /// has none.
151    pub fn horn(&mut self, vehicle: BodyId) -> Sound {
152        match self.physics.vehicle(vehicle).and_then(|v| v.horn_sound.clone()) {
153            Some(name) => self.play_on(vehicle, &name),
154            None => Sound::none(),
155        }
156    }
157
158    /// Starts the engine of a vehicle and plays its start sound. Returns `false` if the
159    /// body is no vehicle, the engine already runs or the tank is empty.
160    pub fn start_engine(&mut self, vehicle: BodyId) -> bool {
161        let Some(v) = self.physics.vehicle_mut(vehicle) else {
162            return false;
163        };
164        if v.is_engine_on() || !v.start_engine() {
165            return false;
166        }
167        if let Some(name) = v.start_sound.clone() {
168            self.play_on(vehicle, &name);
169        }
170        true
171    }
172
173    /// Stops the engine of a vehicle. Its engine sound fades out and it rolls out.
174    pub fn stop_engine(&mut self, vehicle: BodyId) {
175        if let Some(v) = self.physics.vehicle_mut(vehicle) {
176            v.stop_engine();
177        }
178    }
179
180    /// Keeps the engine loops of vehicles playing, and plays shift and crash sounds.
181    /// Called by the engine once per frame.
182    pub(crate) fn sync_vehicles(&mut self) {
183        let (physics, audio) = (&self.physics, &mut self.audio);
184        // Engine loops: start while the engine runs, follow rpm and throttle, stop otherwise.
185        self.engine_sounds.retain(|(id, sound)| {
186            let running = physics.vehicle(*id).is_some_and(|v| v.is_engine_on() && v.engine_sound.is_some());
187            if !running || !sound.is_playing() {
188                sound.fade_out(0.3);
189            }
190            running && sound.is_playing()
191        });
192        for (id, body) in physics.bodies() {
193            let Some(v) = &body.vehicle else {
194                continue;
195            };
196            let Some(name) = &v.engine_sound else {
197                continue;
198            };
199            if !v.is_engine_on() {
200                continue;
201            }
202            let (pitch, volume) = (v.engine_pitch(), 0.6 + 0.4 * v.throttle.clamp(0.0, 1.0));
203            match self.engine_sounds.iter().find(|(e, _)| *e == id) {
204                Some((_, sound)) => {
205                    sound.set_position(body.position);
206                    sound.set_pitch(pitch);
207                    sound.set_volume(volume);
208                }
209                None => {
210                    let sound = audio.sound(name).looped().at(body.position).pitch(pitch).volume(volume).play();
211                    self.engine_sounds.push((id, sound));
212                }
213            }
214        }
215        for event in physics.events() {
216            let (vehicle, name, volume) = match *event {
217                PhysicsEvent::Crashed { vehicle, speed, .. } => {
218                    (vehicle, physics.vehicle(vehicle).and_then(|v| v.crash_sound.as_deref()), (speed / 15.0).min(1.0))
219                }
220                PhysicsEvent::GearChanged { vehicle, gear } if gear != 0 => {
221                    (vehicle, physics.vehicle(vehicle).and_then(|v| v.shift_sound.as_deref()), 1.0)
222                }
223                _ => continue,
224            };
225            if let Some(name) = name {
226                audio.sound(name).at(physics.position(vehicle)).volume(volume).play();
227            }
228        }
229    }
230
231    /// Moves the listener and attached sounds to their bodies. Called by the engine every frame.
232    pub(crate) fn sync_world(&mut self) {
233        if let Some(id) = self.follow {
234            match self.physics.get(id) {
235                Some(body) => {
236                    let (position, facing, pitch) = (body.position, body.facing, body.pitch);
237                    if position != self.audio.listener_position()
238                        || facing != self.audio.listener_yaw()
239                        || pitch != self.audio.listener_pitch()
240                    {
241                        self.audio.set_listener_3d(position, facing, pitch);
242                    }
243                }
244                None => self.follow = None,
245            }
246        }
247        let physics = &self.physics;
248        self.attached.retain_mut(|a| {
249            if !a.sound.is_playing() {
250                return false;
251            }
252            match physics.get(a.body) {
253                Some(body) => {
254                    if body.position != a.last {
255                        a.sound.set_position(body.position);
256                        a.last = body.position;
257                    }
258                    true
259                }
260                None => {
261                    a.sound.stop();
262                    false
263                }
264            }
265        });
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use crate::prelude::*;
272
273    #[test]
274    fn follow_and_attach() {
275        let mut runner = Engine::new("t").headless().build_fn(|_| {});
276        let ctx = &mut runner.ctx;
277        let player = ctx.physics.add(Body::kinematic(vec3(1.0, 2.0, 0.0)).with_facing(90.0));
278        ctx.follow(player);
279        assert_eq!(ctx.audio.listener_position(), vec3(1.0, 2.0, 0.0));
280        assert_eq!(ctx.audio.listener_yaw(), 90.0);
281        ctx.physics.move_forward(player, 3.0);
282        ctx.sync_world();
283        assert!((ctx.audio.listener_position().x - 4.0).abs() < 1e-4);
284
285        assert!(ctx.once("a"));
286        assert!(!ctx.once("a"));
287    }
288}