1use std::sync::Arc;
2use std::time::Duration;
3
4use anyhow::Result;
5use winit::application::ApplicationHandler;
6use winit::event::{ElementState, WindowEvent};
7use winit::event_loop::{ActiveEventLoop, EventLoop};
8use winit::window::{Window, WindowId};
9
10use crate::Game;
11use crate::context::{self, Context, GameEvent, Parts};
12
13#[derive(Clone)]
18pub struct AppConfig {
19 pub title: String,
20 pub width: u32,
21 pub height: u32,
22 pub vsync: bool,
23 pub fixed_timestep: Option<Duration>,
36 pub max_frame_time: Duration,
43 pub asset_root: std::path::PathBuf,
46 pub web_preload: Option<Vec<String>>,
48 pub canvas_id: Option<String>,
51 pub random_seed: u64,
53 pub depth_format: Option<wgpu::TextureFormat>,
57 pub msaa_samples: u32,
60}
61
62impl Default for AppConfig {
63 fn default() -> Self {
64 Self {
65 title: "Game".into(),
66 width: 1280,
67 height: 720,
68 vsync: true,
69 fixed_timestep: None,
70 max_frame_time: Duration::from_millis(250),
71 asset_root: std::path::PathBuf::from("assets"),
72 web_preload: None,
73 canvas_id: None,
74 random_seed: crate::Rng::DEFAULT_SEED,
75 depth_format: None,
76 msaa_samples: 1,
77 }
78 }
79}
80
81type InitDone = Box<(Arc<Window>, Result<Parts>)>;
84
85pub fn run<G: Game>(config: AppConfig) -> Result<()> {
91 #[cfg(target_arch = "wasm32")]
92 crate::web::install_hooks();
93 let event_loop = EventLoop::<InitDone>::with_user_event().build()?;
94 event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
95 #[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
96 let mut runner = AppRunner::<G> {
97 config,
98 state: None,
99 accumulator: Duration::ZERO,
100 #[cfg(target_arch = "wasm32")]
101 proxy: event_loop.create_proxy(),
102 #[cfg(target_arch = "wasm32")]
103 initializing: false,
104 };
105 #[cfg(not(target_arch = "wasm32"))]
106 event_loop.run_app(&mut runner)?;
107 #[cfg(target_arch = "wasm32")]
108 {
109 use winit::platform::web::EventLoopExtWebSys;
110 event_loop.spawn_app(runner);
111 }
112 Ok(())
113}
114
115struct AppRunner<G: Game> {
116 config: AppConfig,
117 state: Option<RunState<G>>,
118 accumulator: Duration,
119 #[cfg(target_arch = "wasm32")]
120 proxy: winit::event_loop::EventLoopProxy<InitDone>,
121 #[cfg(target_arch = "wasm32")]
123 initializing: bool,
124}
125
126struct RunState<G: Game> {
127 ctx: Context,
128 game: G,
129}
130
131impl<G: Game> ApplicationHandler<InitDone> for AppRunner<G> {
132 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
133 if self.state.is_some() {
134 return;
135 }
136 #[cfg(target_arch = "wasm32")]
137 if self.initializing {
138 return;
139 }
140 let window = match context::create_window(event_loop, &self.config) {
141 Ok(w) => w,
142 Err(e) => {
143 log::error!("window creation failed: {e:?}");
144 event_loop.exit();
145 return;
146 }
147 };
148 #[cfg(not(target_arch = "wasm32"))]
151 {
152 let parts =
153 pollster::block_on(context::init_parts(window.clone(), self.config.clone()));
154 self.finish_init(event_loop, window, parts);
155 }
156 #[cfg(target_arch = "wasm32")]
157 {
158 self.initializing = true;
159 let proxy = self.proxy.clone();
160 let config = self.config.clone();
161 wasm_bindgen_futures::spawn_local(async move {
162 let parts = context::init_parts(window.clone(), config).await;
163 if proxy.send_event(Box::new((window, parts))).is_err() {
164 log::error!("event loop closed before init finished");
165 }
166 });
167 }
168 }
169
170 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: InitDone) {
171 let (window, parts) = *event;
172 #[cfg(target_arch = "wasm32")]
173 {
174 self.initializing = false;
175 }
176 self.finish_init(event_loop, window, parts);
177 }
178
179 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
180 let Some(state) = self.state.as_mut() else {
181 return;
182 };
183 if is_user_gesture(&event) {
186 state.ctx.start_audio();
187 }
188 let consumed = state.game.raw_window_event(&mut state.ctx, &event);
190 match event {
191 WindowEvent::CloseRequested => {
192 state.game.event(&mut state.ctx, &GameEvent::CloseRequested);
193 event_loop.exit();
194 }
195 WindowEvent::Resized(size) => {
196 state.ctx.gfx.resize(size.width, size.height);
197 state.game.event(
198 &mut state.ctx,
199 &GameEvent::Resized {
200 width: size.width,
201 height: size.height,
202 },
203 );
204 }
205 WindowEvent::Focused(focused) => {
206 if focused {
207 self.accumulator = Duration::ZERO;
209 }
210 state
211 .ctx
212 .input
213 .handle_window_event(&WindowEvent::Focused(focused));
214 state
215 .game
216 .event(&mut state.ctx, &GameEvent::FocusChanged(focused));
217 }
218 WindowEvent::RedrawRequested => {
219 self.frame(event_loop);
220 }
221 ref other if !consumed => {
222 state.ctx.input.handle_window_event(other);
223 }
224 _ => {}
225 }
226 }
227
228 fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
229 if let Some(state) = self.state.as_ref() {
230 state.ctx.window.request_redraw();
231 }
232 }
233
234 fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
235 if let Some(state) = self.state.as_mut() {
236 state.game.shutdown(&mut state.ctx);
237 }
238 }
239}
240
241fn alpha(accumulator: Duration, step: Duration) -> f32 {
243 if step.is_zero() {
244 return 0.0;
245 }
246 (accumulator.as_secs_f64() / step.as_secs_f64()).clamp(0.0, 1.0) as f32
247}
248
249fn is_user_gesture(event: &WindowEvent) -> bool {
250 match event {
251 WindowEvent::KeyboardInput { event, .. } => event.state == ElementState::Pressed,
252 WindowEvent::MouseInput { state, .. } => *state == ElementState::Pressed,
253 WindowEvent::Touch(_) => true,
254 _ => false,
255 }
256}
257
258impl<G: Game> AppRunner<G> {
259 fn finish_init(
262 &mut self,
263 event_loop: &ActiveEventLoop,
264 window: Arc<Window>,
265 parts: Result<Parts>,
266 ) {
267 let parts = match parts {
268 Ok(p) => p,
269 Err(e) => {
270 log::error!("context init failed: {e:?}");
271 event_loop.exit();
272 return;
273 }
274 };
275 let mut ctx = Context::from_parts(window, parts, &self.config);
276 let size = ctx.window.inner_size();
280 ctx.gfx.resize(size.width, size.height);
281 let game = match G::init(&mut ctx) {
282 Ok(g) => g,
283 Err(e) => {
284 log::error!("game init failed: {e:?}");
285 event_loop.exit();
286 return;
287 }
288 };
289 ctx.window.request_redraw();
290 self.state = Some(RunState { ctx, game });
291 }
292
293 fn frame(&mut self, event_loop: &ActiveEventLoop) {
294 let Some(state) = self.state.as_mut() else {
295 return;
296 };
297 state.ctx.time.tick();
298 state.ctx.input.poll_gamepads();
299
300 let delta = match self.config.max_frame_time {
301 Duration::ZERO => state.ctx.time.delta,
302 cap => state.ctx.time.delta.min(cap),
303 };
304 match self.config.fixed_timestep {
305 Some(step) => {
306 self.accumulator += delta;
307 let mut updated = false;
308 while self.accumulator >= step {
309 state.game.update(&mut state.ctx, step.as_secs_f32());
310 self.accumulator -= step;
311 if !updated {
314 state.ctx.input.end_frame();
315 updated = true;
316 }
317 }
318 state.ctx.time.alpha = alpha(self.accumulator, step);
319 }
320 None => {
321 state.game.update(&mut state.ctx, delta.as_secs_f32());
322 state.ctx.input.end_frame();
323 }
324 }
325
326 if state.ctx.quit_requested {
327 event_loop.exit();
328 return;
329 }
330
331 match state.ctx.gfx.begin_frame() {
332 Ok(mut frame) => {
333 state.game.render(&mut state.ctx, &mut frame);
334 state.ctx.gfx.present(frame);
335 }
336 Err(e) => {
337 log::warn!("begin_frame failed: {e:?}");
338 let (w, h) = state.ctx.gfx.size();
339 state.ctx.gfx.resize(w, h);
340 }
341 }
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 #[test]
350 fn alpha_is_fraction_of_step() {
351 let step = Duration::from_millis(10);
352 assert_eq!(alpha(Duration::ZERO, step), 0.0);
353 assert!((alpha(Duration::from_millis(5), step) - 0.5).abs() < 1e-6);
354 assert_eq!(alpha(Duration::from_millis(5), Duration::ZERO), 0.0);
355 }
356}