Skip to main content

game_toolkit_core/
app.rs

1use std::time::Duration;
2
3use anyhow::Result;
4use winit::application::ApplicationHandler;
5use winit::event::WindowEvent;
6use winit::event_loop::{ActiveEventLoop, EventLoop};
7use winit::window::WindowId;
8
9use crate::Game;
10use crate::context::{Context, GameEvent};
11
12/// Runtime configuration passed to [`run`].
13///
14/// Construct it with `..Default::default()` so new fields added in future minor releases don't
15/// break your call site: `AppConfig { title: "game".into(), ..Default::default() }`.
16#[derive(Clone)]
17pub struct AppConfig {
18    pub title: String,
19    pub width: u32,
20    pub height: u32,
21    pub vsync: bool,
22    /// `Some(d)` for deterministic fixed-step `update`; `None` for variable `dt`.
23    pub fixed_timestep: Option<Duration>,
24    /// Asset root directory. Defaults to `./assets`.
25    pub asset_root: std::path::PathBuf,
26    /// Seed for deterministic runtime RNG (`ctx.rng`).
27    pub random_seed: u64,
28    /// Depth attachment format. `None` (default) keeps the 2D path depth-less; `Some(fmt)`
29    /// (e.g. `wgpu::TextureFormat::Depth32Float`) allocates a depth buffer for depth-tested
30    /// rendering. The built-in 2D pipelines never write depth, so enabling it is harmless.
31    pub depth_format: Option<wgpu::TextureFormat>,
32    /// MSAA sample count for the surface. `1` (default) disables MSAA; `2`/`4`/`8` enable it
33    /// (the value must be supported by the adapter for the surface format).
34    pub msaa_samples: u32,
35}
36
37impl Default for AppConfig {
38    fn default() -> Self {
39        Self {
40            title: "Game".into(),
41            width: 1280,
42            height: 720,
43            vsync: true,
44            fixed_timestep: None,
45            asset_root: std::path::PathBuf::from("assets"),
46            random_seed: crate::Rng::DEFAULT_SEED,
47            depth_format: None,
48            msaa_samples: 1,
49        }
50    }
51}
52
53pub fn run<G: Game>(config: AppConfig) -> Result<()> {
54    let event_loop = EventLoop::new()?;
55    event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
56    let mut runner = AppRunner::<G> {
57        config,
58        state: None,
59        accumulator: Duration::ZERO,
60    };
61    event_loop.run_app(&mut runner)?;
62    Ok(())
63}
64
65struct AppRunner<G: Game> {
66    config: AppConfig,
67    state: Option<RunState<G>>,
68    accumulator: Duration,
69}
70
71struct RunState<G: Game> {
72    ctx: Context,
73    game: G,
74}
75
76impl<G: Game> ApplicationHandler for AppRunner<G> {
77    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
78        if self.state.is_some() {
79            return;
80        }
81        let mut ctx = match Context::new(
82            event_loop,
83            &self.config.title,
84            self.config.width,
85            self.config.height,
86            self.config.vsync,
87            self.config.asset_root.clone(),
88            self.config.random_seed,
89            self.config.depth_format,
90            self.config.msaa_samples,
91        ) {
92            Ok(c) => c,
93            Err(e) => {
94                log::error!("context init failed: {e:?}");
95                event_loop.exit();
96                return;
97            }
98        };
99        let game = match G::init(&mut ctx) {
100            Ok(g) => g,
101            Err(e) => {
102                log::error!("game init failed: {e:?}");
103                event_loop.exit();
104                return;
105            }
106        };
107        ctx.window.request_redraw();
108        self.state = Some(RunState { ctx, game });
109    }
110
111    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
112        let Some(state) = self.state.as_mut() else {
113            return;
114        };
115        // Give the game's raw-event hook first crack (egui etc. needs this).
116        let consumed = state.game.raw_window_event(&mut state.ctx, &event);
117        match event {
118            WindowEvent::CloseRequested => {
119                state.game.event(&mut state.ctx, &GameEvent::CloseRequested);
120                event_loop.exit();
121            }
122            WindowEvent::Resized(size) => {
123                state.ctx.gfx.resize(size.width, size.height);
124                state.game.event(
125                    &mut state.ctx,
126                    &GameEvent::Resized {
127                        width: size.width,
128                        height: size.height,
129                    },
130                );
131            }
132            WindowEvent::Focused(focused) => {
133                state
134                    .ctx
135                    .input
136                    .handle_window_event(&WindowEvent::Focused(focused));
137                state
138                    .game
139                    .event(&mut state.ctx, &GameEvent::FocusChanged(focused));
140            }
141            WindowEvent::RedrawRequested => {
142                self.frame(event_loop);
143            }
144            ref other if !consumed => {
145                state.ctx.input.handle_window_event(other);
146            }
147            _ => {}
148        }
149    }
150
151    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
152        if let Some(state) = self.state.as_ref() {
153            state.ctx.window.request_redraw();
154        }
155    }
156
157    fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
158        if let Some(state) = self.state.as_mut() {
159            state.game.shutdown(&mut state.ctx);
160        }
161    }
162}
163
164impl<G: Game> AppRunner<G> {
165    fn frame(&mut self, event_loop: &ActiveEventLoop) {
166        let Some(state) = self.state.as_mut() else {
167            return;
168        };
169        state.ctx.time.tick();
170        state.ctx.input.poll_gamepads();
171
172        match self.config.fixed_timestep {
173            Some(step) => {
174                self.accumulator += state.ctx.time.delta;
175                while self.accumulator >= step {
176                    state.game.update(&mut state.ctx, step.as_secs_f32());
177                    self.accumulator -= step;
178                }
179            }
180            None => {
181                let dt = state.ctx.time.delta.as_secs_f32();
182                state.game.update(&mut state.ctx, dt);
183            }
184        }
185
186        if state.ctx.quit_requested {
187            event_loop.exit();
188            return;
189        }
190
191        state.ctx.input.end_frame();
192
193        match state.ctx.gfx.begin_frame() {
194            Ok(mut frame) => {
195                state.game.render(&mut state.ctx, &mut frame);
196                state.ctx.gfx.present(frame);
197            }
198            Err(e) => {
199                log::warn!("begin_frame failed: {e:?}");
200                let (w, h) = state.ctx.gfx.size();
201                state.ctx.gfx.resize(w, h);
202            }
203        }
204    }
205}