Skip to main content

audio_code/
engine.rs

1//! The game loop: [`Engine`], [`Scene`] and [`Context`].
2
3use std::any::{Any, TypeId};
4use std::collections::{HashMap, HashSet};
5use std::path::PathBuf;
6use std::time::{Duration, Instant};
7
8use crate::audio::{Audio, Sound};
9use crate::error::Result;
10use crate::input::{Input, Key, KeyEvent};
11use crate::physics::{BodyId, World};
12use crate::shortcuts::Attached;
13use crate::speech::Speech;
14use crate::storage::Storage;
15use crate::time::Time;
16use crate::ui::{Ui, UiEvent};
17
18/// One screen or state of your game: main menu, a level, the game over screen...
19///
20/// Only [`Scene::update`] is required. Scenes are switched with [`Context::switch_scene`],
21/// stacked with [`Context::push_scene`] (e.g. a pause screen) and removed with
22/// [`Context::pop_scene`].
23pub trait Scene: 'static {
24    /// Called once when the scene becomes active for the first time.
25    fn start(&mut self, ctx: &mut Context) {
26        let _ = ctx;
27    }
28
29    /// Called every frame. Put your game logic here.
30    fn update(&mut self, ctx: &mut Context);
31
32    /// Called at a fixed rate (60 times per second by default) right after each physics step.
33    /// Use it for logic that must not depend on the frame rate.
34    fn fixed_update(&mut self, ctx: &mut Context) {
35        let _ = ctx;
36    }
37
38    /// Called for every UI event before [`Scene::update`]. An alternative to polling
39    /// `ctx.ui.clicked(...)` and friends in `update`.
40    fn on_ui(&mut self, event: &UiEvent, ctx: &mut Context) {
41        let _ = (event, ctx);
42    }
43
44    /// Called when another scene is pushed on top of this one.
45    fn pause(&mut self, ctx: &mut Context) {
46        let _ = ctx;
47    }
48
49    /// Called when this scene is on top again after the scene above was popped.
50    fn resume(&mut self, ctx: &mut Context) {
51        let _ = ctx;
52    }
53
54    /// Called when the scene is removed (popped, switched away, or the game quits).
55    fn end(&mut self, ctx: &mut Context) {
56        let _ = ctx;
57    }
58}
59
60enum SceneChange {
61    Push(Box<dyn Scene>),
62    Pop,
63    Switch(Box<dyn Scene>),
64}
65
66/// Everything your game can use, passed to every [`Scene`] method.
67pub struct Context {
68    /// Frame timing and scheduling.
69    pub time: Time,
70    /// Keyboard state.
71    pub input: Input,
72    /// Sound effects, positioned sounds and music.
73    pub audio: Audio,
74    /// Screen reader / text-to-speech output.
75    pub speech: Speech,
76    /// Virtual menus and dialogs.
77    pub ui: Ui,
78    /// Physics world.
79    pub physics: World,
80    /// Values that are saved when the game quits (high scores, settings).
81    pub storage: Storage,
82    pub(crate) follow: Option<BodyId>,
83    pub(crate) attached: Vec<Attached>,
84    pub(crate) engine_sounds: Vec<(BodyId, Sound)>,
85    pub(crate) once: HashSet<String>,
86    title: String,
87    quit: bool,
88    change: Option<SceneChange>,
89    globals: HashMap<TypeId, Box<dyn Any>>,
90}
91
92impl std::fmt::Debug for Context {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        f.debug_struct("Context").field("title", &self.title).field("time", &self.time).finish_non_exhaustive()
95    }
96}
97
98impl Context {
99    /// Requests the game to end after this frame.
100    pub fn quit(&mut self) {
101        self.quit = true;
102    }
103
104    /// Whether [`Context::quit`] was called.
105    pub fn is_quitting(&self) -> bool {
106        self.quit
107    }
108
109    /// The game title given to [`Engine::new`].
110    pub fn title(&self) -> &str {
111        &self.title
112    }
113
114    /// Replaces the current scene with another one after this frame.
115    pub fn switch_scene(&mut self, scene: impl Scene) {
116        self.change = Some(SceneChange::Switch(Box::new(scene)));
117    }
118
119    /// Puts a scene on top of the current one (which is paused) after this frame.
120    pub fn push_scene(&mut self, scene: impl Scene) {
121        self.change = Some(SceneChange::Push(Box::new(scene)));
122    }
123
124    /// Removes the current scene after this frame. Popping the last scene quits the game.
125    pub fn pop_scene(&mut self) {
126        self.change = Some(SceneChange::Pop);
127    }
128
129    /// Stores a value shared by all scenes (settings, score, save data...).
130    /// There is one slot per type; storing again replaces the old value.
131    pub fn insert<T: 'static>(&mut self, value: T) {
132        self.globals.insert(TypeId::of::<T>(), Box::new(value));
133    }
134
135    /// Reads a shared value stored with [`Context::insert`].
136    pub fn get<T: 'static>(&self) -> Option<&T> {
137        self.globals.get(&TypeId::of::<T>()).and_then(|b| b.downcast_ref())
138    }
139
140    /// Mutable access to a shared value.
141    pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
142        self.globals.get_mut(&TypeId::of::<T>()).and_then(|b| b.downcast_mut())
143    }
144
145    /// Mutable access to a shared value, inserting `T::default()` if it is missing.
146    pub fn get_or_default<T: Default + 'static>(&mut self) -> &mut T {
147        self.globals
148            .entry(TypeId::of::<T>())
149            .or_insert_with(|| Box::new(T::default()))
150            .downcast_mut()
151            .expect("type id matches")
152    }
153
154    /// Removes a shared value and returns it.
155    pub fn remove<T: 'static>(&mut self) -> Option<T> {
156        self.globals.remove(&TypeId::of::<T>()).and_then(|b| b.downcast().ok()).map(|b| *b)
157    }
158}
159
160/// Configures and starts the game.
161///
162/// ```no_run
163/// use audio_code::prelude::*;
164///
165/// struct Hello;
166///
167/// impl Scene for Hello {
168///     fn start(&mut self, ctx: &mut Context) {
169///         ctx.speech.speak("Hello! Press space to beep, escape to quit.");
170///     }
171///
172///     fn update(&mut self, ctx: &mut Context) {
173///         if ctx.input.pressed(Key::Space) {
174///             ctx.audio.play_tone(440.0, 0.2);
175///         }
176///         if ctx.input.pressed(Key::Escape) {
177///             ctx.quit();
178///         }
179///     }
180/// }
181///
182/// fn main() -> audio_code::Result<()> {
183///     Engine::new("Hello").run(Hello)
184/// }
185/// ```
186#[derive(Debug, Clone)]
187pub struct Engine {
188    pub(crate) title: String,
189    pub(crate) fps: u32,
190    pub(crate) physics_rate: u32,
191    pub(crate) sound_dir: PathBuf,
192    pub(crate) echo_speech: bool,
193    pub(crate) window_size: (u32, u32),
194    pub(crate) headless: bool,
195    pub(crate) gravity: glam::Vec3,
196    pub(crate) save_file: Option<PathBuf>,
197    pub(crate) save_secret: Option<String>,
198}
199
200impl Engine {
201    /// Creates a new engine configuration. The title is used for the window and is what the
202    /// screen reader announces when the window gets focus.
203    pub fn new(title: impl Into<String>) -> Self {
204        Self {
205            title: title.into(),
206            fps: 60,
207            physics_rate: 60,
208            sound_dir: PathBuf::from("sounds"),
209            echo_speech: false,
210            window_size: (640, 480),
211            headless: false,
212            gravity: glam::Vec3::ZERO,
213            save_file: None,
214            save_secret: None,
215        }
216    }
217
218    /// Target frames per second (default 60).
219    pub fn fps(mut self, fps: u32) -> Self {
220        self.fps = fps.clamp(1, 1000);
221        self
222    }
223
224    /// Physics and [`Scene::fixed_update`] steps per second (default 60).
225    pub fn physics_rate(mut self, rate: u32) -> Self {
226        self.physics_rate = rate.clamp(1, 1000);
227        self
228    }
229
230    /// Gravity of the physics world, e.g. [`EARTH_GRAVITY`](crate::physics::EARTH_GRAVITY).
231    /// Default is none (top down games). Can be changed later with `ctx.physics.set_gravity`.
232    pub fn gravity(mut self, gravity: glam::Vec3) -> Self {
233        self.gravity = gravity;
234        self
235    }
236
237    /// File used by [`Context::storage`] (default `<title>.save` in the working directory).
238    pub fn save_file(mut self, path: impl Into<PathBuf>) -> Self {
239        self.save_file = Some(path.into());
240        self
241    }
242
243    /// Encrypts the save file of [`Context::storage`] with a key made from `secret`
244    /// (optional, default off). Players can no longer read or edit it, and a changed or
245    /// damaged file is detected and kept as `<file>.broken`. An existing plain text save is
246    /// converted on the next save. See [`crate::storage`] for what this protects against.
247    pub fn encrypt_save(mut self, secret: impl Into<String>) -> Self {
248        self.save_secret = Some(secret.into());
249        self
250    }
251
252    /// Folder searched for sounds (default `"sounds"`). See [`crate::audio`].
253    pub fn sound_dir(mut self, dir: impl Into<PathBuf>) -> Self {
254        self.sound_dir = dir.into();
255        self
256    }
257
258    /// Print everything spoken to the console as well (default `false`).
259    pub fn echo_speech(mut self, echo: bool) -> Self {
260        self.echo_speech = echo;
261        self
262    }
263
264    /// Window size in pixels (default 640×480). The window shows nothing, but sighted
265    /// helpers can still find and focus it.
266    pub fn window_size(mut self, width: u32, height: u32) -> Self {
267        self.window_size = (width, height);
268        self
269    }
270
271    /// Use no audio device and print speech to the console instead of using prism.
272    /// Meant for automated tests and tools.
273    pub fn headless(mut self) -> Self {
274        self.headless = true;
275        self
276    }
277
278    /// Opens the window and runs the game until it quits.
279    pub fn run(self, scene: impl Scene) -> Result<()> {
280        crate::window::run(self, Box::new(scene))
281    }
282
283    /// Like [`Engine::run`], but builds the first scene with access to the [`Context`].
284    /// Handy when the scene needs to create bodies or load sounds in its constructor:
285    ///
286    /// ```no_run
287    /// # use audio_code::prelude::*;
288    /// struct Level { player: BodyId }
289    ///
290    /// impl Level {
291    ///     fn new(ctx: &mut Context) -> Self {
292    ///         Self { player: ctx.physics.add(Body::kinematic(Vec3::ZERO)) }
293    ///     }
294    /// }
295    /// # impl Scene for Level { fn update(&mut self, _: &mut Context) {} }
296    ///
297    /// fn main() -> audio_code::Result<()> {
298    ///     Engine::new("Game").run_with(Level::new)
299    /// }
300    /// ```
301    pub fn run_with<S: Scene>(self, build: impl FnOnce(&mut Context) -> S + 'static) -> Result<()> {
302        self.run(Lazy::new(build))
303    }
304
305    /// Like [`Engine::run`], but uses a native wxWidgets frame (feature `wxdragon`).
306    /// See [`crate::wx`] for embedding a game into an existing wxDragon application.
307    #[cfg(feature = "wxdragon")]
308    pub fn run_wx(self, scene: impl Scene) -> Result<()> {
309        crate::wx::run(self, Box::new(scene))
310    }
311
312    /// Like [`Engine::run_with`] for a wxWidgets frame (feature `wxdragon`).
313    #[cfg(feature = "wxdragon")]
314    pub fn run_wx_with<S: Scene>(self, build: impl FnOnce(&mut Context) -> S + 'static) -> Result<()> {
315        self.run_wx(Lazy::new(build))
316    }
317
318    /// Runs a game written as a single closure that is called every frame.
319    /// The closure can keep its own state in captured variables. See [`crate::run`].
320    pub fn run_fn(self, update: impl FnMut(&mut Context) + 'static) -> Result<()> {
321        self.run(FnScene(update))
322    }
323
324    /// Like [`Engine::build`] for a closure game.
325    pub fn build_fn(self, update: impl FnMut(&mut Context) + 'static) -> Runner {
326        self.build(FnScene(update))
327    }
328
329    /// Creates the [`Context`] and runner without a window. Window backends use this; you
330    /// can use it for tests or to drive the engine from your own loop.
331    pub fn build(self, scene: impl Scene) -> Runner {
332        Runner::new(self, Box::new(scene))
333    }
334}
335
336/// A scene made from a closure.
337struct FnScene<F>(F);
338
339impl<F: FnMut(&mut Context) + 'static> Scene for FnScene<F> {
340    fn update(&mut self, ctx: &mut Context) {
341        (self.0)(ctx);
342    }
343}
344
345/// A scene that is built on its first `start` call.
346struct Lazy<S, F> {
347    build: Option<F>,
348    scene: Option<S>,
349}
350
351impl<S: Scene, F: FnOnce(&mut Context) -> S + 'static> Lazy<S, F> {
352    fn new(build: F) -> Self {
353        Self { build: Some(build), scene: None }
354    }
355}
356
357impl<S: Scene, F: FnOnce(&mut Context) -> S + 'static> Scene for Lazy<S, F> {
358    fn start(&mut self, ctx: &mut Context) {
359        if let Some(build) = self.build.take() {
360            let mut scene = build(ctx);
361            scene.start(ctx);
362            self.scene = Some(scene);
363        }
364    }
365    fn update(&mut self, ctx: &mut Context) {
366        if let Some(s) = &mut self.scene {
367            s.update(ctx);
368        }
369    }
370    fn fixed_update(&mut self, ctx: &mut Context) {
371        if let Some(s) = &mut self.scene {
372            s.fixed_update(ctx);
373        }
374    }
375    fn on_ui(&mut self, event: &UiEvent, ctx: &mut Context) {
376        if let Some(s) = &mut self.scene {
377            s.on_ui(event, ctx);
378        }
379    }
380    fn pause(&mut self, ctx: &mut Context) {
381        if let Some(s) = &mut self.scene {
382            s.pause(ctx);
383        }
384    }
385    fn resume(&mut self, ctx: &mut Context) {
386        if let Some(s) = &mut self.scene {
387            s.resume(ctx);
388        }
389    }
390    fn end(&mut self, ctx: &mut Context) {
391        if let Some(s) = &mut self.scene {
392            s.end(ctx);
393        }
394    }
395}
396
397/// Drives scenes frame by frame. Window backends feed it key events and call
398/// [`Runner::frame`]. You only need this when writing your own backend or tests.
399pub struct Runner {
400    /// The context passed to scenes.
401    pub ctx: Context,
402    scenes: Vec<Box<dyn Scene>>,
403    started: bool,
404    accumulator: f32,
405    fixed_dt: f32,
406    frame_time: Duration,
407    next_frame: Instant,
408}
409
410impl std::fmt::Debug for Runner {
411    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
412        f.debug_struct("Runner").field("scenes", &self.scenes.len()).finish_non_exhaustive()
413    }
414}
415
416impl Runner {
417    pub(crate) fn new(config: Engine, scene: Box<dyn Scene>) -> Self {
418        let ctx = Context {
419            time: Time::new(),
420            input: Input::default(),
421            audio: if config.headless {
422                Audio::silent(config.sound_dir.clone())
423            } else {
424                Audio::new(config.sound_dir.clone())
425            },
426            speech: if config.headless { Speech::console() } else { Speech::new(config.echo_speech) },
427            ui: Ui::default(),
428            physics: {
429                let mut world = World::new();
430                world.set_gravity(config.gravity);
431                world
432            },
433            storage: if config.headless {
434                Storage::in_memory()
435            } else {
436                let file = config.save_file.clone().unwrap_or_else(|| crate::storage::default_file(&config.title));
437                match &config.save_secret {
438                    Some(secret) => Storage::open_encrypted(file, secret),
439                    None => Storage::open(file),
440                }
441            },
442            follow: None,
443            attached: Vec::new(),
444            engine_sounds: Vec::new(),
445            once: HashSet::new(),
446            title: config.title.clone(),
447            quit: false,
448            change: None,
449            globals: HashMap::new(),
450        };
451        Self {
452            ctx,
453            scenes: vec![scene],
454            started: false,
455            accumulator: 0.0,
456            fixed_dt: 1.0 / config.physics_rate as f32,
457            frame_time: Duration::from_secs_f64(1.0 / config.fps as f64),
458            next_frame: Instant::now(),
459        }
460    }
461
462    /// Reports a key going down. `repeat` is `true` for OS key repeats.
463    pub fn key_down(&mut self, key: Key, repeat: bool) {
464        self.ctx.input.push(KeyEvent::Down { key, repeat });
465    }
466
467    /// Reports a key going up.
468    pub fn key_up(&mut self, key: Key) {
469        self.ctx.input.push(KeyEvent::Up { key });
470    }
471
472    /// Reports typed text.
473    pub fn text(&mut self, text: &str) {
474        self.ctx.input.push_text(text);
475    }
476
477    /// Releases all keys (call when the window loses focus).
478    pub fn focus_lost(&mut self) {
479        self.ctx.input.release_all();
480    }
481
482    /// When the next frame is due.
483    pub fn next_frame_time(&self) -> Instant {
484        self.next_frame
485    }
486
487    /// Whether the game wants to quit.
488    pub fn should_quit(&self) -> bool {
489        self.ctx.quit
490    }
491
492    /// Runs one frame now. Returns `false` when the game has quit.
493    pub fn frame(&mut self) -> bool {
494        let now = Instant::now();
495        // Schedule the next frame, but never try to catch up on missed ones.
496        self.next_frame = (self.next_frame + self.frame_time).max(now);
497
498        if !self.started {
499            self.started = true;
500            self.ctx.time.advance(now);
501            if let Some(scene) = self.scenes.last_mut() {
502                scene.start(&mut self.ctx);
503            }
504            self.apply_change();
505            return !self.ctx.quit;
506        }
507
508        let ctx = &mut self.ctx;
509        ctx.time.advance(now);
510        ctx.input.begin_frame(now);
511
512        let consumed = ctx.ui.update(&ctx.input, &mut ctx.speech, &mut ctx.audio);
513        ctx.input.set_blocked(consumed || ctx.ui.is_open());
514
515        if let Some(scene) = self.scenes.last_mut() {
516            for event in self.ctx.ui.take_events() {
517                scene.on_ui(&event, &mut self.ctx);
518            }
519
520            self.ctx.physics.clear_events();
521            self.accumulator += self.ctx.time.delta();
522            let mut steps = 0;
523            while self.accumulator >= self.fixed_dt && steps < 8 {
524                self.accumulator -= self.fixed_dt;
525                self.ctx.physics.step(self.fixed_dt);
526                scene.fixed_update(&mut self.ctx);
527                steps += 1;
528            }
529            if steps == 8 {
530                self.accumulator = 0.0;
531            }
532
533            scene.update(&mut self.ctx);
534        }
535        self.ctx.sync_world();
536        self.ctx.sync_vehicles();
537        self.apply_change();
538
539        if self.ctx.quit {
540            self.shutdown();
541            return false;
542        }
543        true
544    }
545
546    fn apply_change(&mut self) {
547        // A scene's start() may itself request another change, so loop.
548        while let Some(change) = self.ctx.change.take() {
549            match change {
550                SceneChange::Push(mut scene) => {
551                    if let Some(top) = self.scenes.last_mut() {
552                        top.pause(&mut self.ctx);
553                    }
554                    scene.start(&mut self.ctx);
555                    self.scenes.push(scene);
556                }
557                SceneChange::Pop => {
558                    if let Some(mut top) = self.scenes.pop() {
559                        top.end(&mut self.ctx);
560                    }
561                    match self.scenes.last_mut() {
562                        Some(top) => top.resume(&mut self.ctx),
563                        None => self.ctx.quit = true,
564                    }
565                }
566                SceneChange::Switch(mut scene) => {
567                    if let Some(mut top) = self.scenes.pop() {
568                        top.end(&mut self.ctx);
569                    }
570                    scene.start(&mut self.ctx);
571                    self.scenes.push(scene);
572                }
573            }
574        }
575    }
576
577    /// Ends all scenes. Called automatically when the game quits.
578    pub fn shutdown(&mut self) {
579        while let Some(mut scene) = self.scenes.pop() {
580            scene.end(&mut self.ctx);
581        }
582        if let Err(e) = self.ctx.storage.save() {
583            eprintln!("audio-code: could not save storage: {e}");
584        }
585        self.ctx.quit = true;
586    }
587}
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592    use std::cell::RefCell;
593    use std::rc::Rc;
594
595    struct Recorder(Rc<RefCell<Vec<String>>>, &'static str);
596
597    impl Scene for Recorder {
598        fn start(&mut self, _: &mut Context) {
599            self.0.borrow_mut().push(format!("{} start", self.1));
600        }
601        fn update(&mut self, ctx: &mut Context) {
602            self.0.borrow_mut().push(format!("{} update", self.1));
603            if self.1 == "a" && ctx.input.pressed(Key::Enter) {
604                ctx.push_scene(Recorder(self.0.clone(), "b"));
605            }
606            if self.1 == "b" && ctx.input.pressed(Key::Escape) {
607                ctx.pop_scene();
608            }
609        }
610        fn pause(&mut self, _: &mut Context) {
611            self.0.borrow_mut().push(format!("{} pause", self.1));
612        }
613        fn resume(&mut self, _: &mut Context) {
614            self.0.borrow_mut().push(format!("{} resume", self.1));
615        }
616        fn end(&mut self, _: &mut Context) {
617            self.0.borrow_mut().push(format!("{} end", self.1));
618        }
619    }
620
621    #[test]
622    fn scene_stack() {
623        let log = Rc::new(RefCell::new(Vec::new()));
624        let mut runner = Engine::new("test").headless().build(Recorder(log.clone(), "a"));
625        runner.frame();
626        runner.key_down(Key::Enter, false);
627        runner.frame();
628        runner.key_up(Key::Enter);
629        runner.key_down(Key::Escape, false);
630        runner.frame();
631        runner.key_up(Key::Escape);
632        runner.key_down(Key::Escape, false);
633        runner.frame();
634        assert_eq!(
635            *log.borrow(),
636            ["a start", "a update", "a pause", "b start", "b update", "b end", "a resume", "a update"]
637        );
638    }
639
640    #[test]
641    fn globals() {
642        let mut runner = Engine::new("test").headless().build(Recorder(Rc::default(), "x"));
643        runner.ctx.insert(5u32);
644        *runner.ctx.get_mut::<u32>().unwrap() += 1;
645        assert_eq!(runner.ctx.get::<u32>(), Some(&6));
646        assert_eq!(*runner.ctx.get_or_default::<String>(), "");
647    }
648}