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