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 pub touch_emulates_mouse: bool,
66}
67
68impl Default for AppConfig {
69 fn default() -> Self {
70 Self {
71 title: "Game".into(),
72 width: 1280,
73 height: 720,
74 vsync: true,
75 fixed_timestep: None,
76 max_frame_time: Duration::from_millis(250),
77 asset_root: std::path::PathBuf::from("assets"),
78 web_preload: None,
79 canvas_id: None,
80 random_seed: crate::Rng::DEFAULT_SEED,
81 depth_format: None,
82 msaa_samples: 1,
83 touch_emulates_mouse: false,
84 }
85 }
86}
87
88type InitDone = Box<(Arc<Window>, Result<Parts>)>;
91
92pub fn run<G: Game>(config: AppConfig) -> Result<()> {
98 #[cfg(target_arch = "wasm32")]
99 crate::web::install_hooks();
100 let event_loop = EventLoop::<InitDone>::with_user_event().build()?;
101 event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
102 #[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
103 let mut runner = AppRunner::<G> {
104 config,
105 state: None,
106 accumulator: Duration::ZERO,
107 #[cfg(target_arch = "wasm32")]
108 proxy: event_loop.create_proxy(),
109 #[cfg(target_arch = "wasm32")]
110 initializing: false,
111 };
112 #[cfg(not(target_arch = "wasm32"))]
113 event_loop.run_app(&mut runner)?;
114 #[cfg(target_arch = "wasm32")]
115 {
116 use winit::platform::web::EventLoopExtWebSys;
117 event_loop.spawn_app(runner);
118 }
119 Ok(())
120}
121
122struct AppRunner<G: Game> {
123 config: AppConfig,
124 state: Option<RunState<G>>,
125 accumulator: Duration,
126 #[cfg(target_arch = "wasm32")]
127 proxy: winit::event_loop::EventLoopProxy<InitDone>,
128 #[cfg(target_arch = "wasm32")]
130 initializing: bool,
131}
132
133struct RunState<G: Game> {
134 ctx: Context,
135 game: G,
136 #[cfg(target_arch = "wasm32")]
138 canvas_watch: Option<crate::web::CanvasWatch>,
139}
140
141impl<G: Game> ApplicationHandler<InitDone> for AppRunner<G> {
142 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
143 if self.state.is_some() {
144 return;
145 }
146 #[cfg(target_arch = "wasm32")]
147 if self.initializing {
148 return;
149 }
150 let window = match context::create_window(event_loop, &self.config) {
151 Ok(w) => w,
152 Err(e) => {
153 log::error!("window creation failed: {e:?}");
154 event_loop.exit();
155 return;
156 }
157 };
158 #[cfg(not(target_arch = "wasm32"))]
161 {
162 let parts =
163 pollster::block_on(context::init_parts(window.clone(), self.config.clone()));
164 self.finish_init(event_loop, window, parts);
165 }
166 #[cfg(target_arch = "wasm32")]
167 {
168 self.initializing = true;
169 let proxy = self.proxy.clone();
170 let config = self.config.clone();
171 wasm_bindgen_futures::spawn_local(async move {
172 let parts = context::init_parts(window.clone(), config).await;
173 if proxy.send_event(Box::new((window, parts))).is_err() {
174 log::error!("event loop closed before init finished");
175 }
176 });
177 }
178 }
179
180 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: InitDone) {
181 let (window, parts) = *event;
182 #[cfg(target_arch = "wasm32")]
183 {
184 self.initializing = false;
185 }
186 self.finish_init(event_loop, window, parts);
187 }
188
189 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
190 let Some(state) = self.state.as_mut() else {
191 return;
192 };
193 if is_user_gesture(&event) {
196 state.ctx.start_audio();
197 }
198 let consumed = state.game.raw_window_event(&mut state.ctx, &event);
200 match event {
201 WindowEvent::CloseRequested => {
202 state.game.event(&mut state.ctx, &GameEvent::CloseRequested);
203 event_loop.exit();
204 }
205 WindowEvent::Resized(size) => {
206 state.ctx.gfx.resize(size.width, size.height);
207 state.game.event(
208 &mut state.ctx,
209 &GameEvent::Resized {
210 width: size.width,
211 height: size.height,
212 },
213 );
214 }
215 WindowEvent::Focused(focused) => {
216 if focused {
217 self.accumulator = Duration::ZERO;
219 }
220 state
221 .ctx
222 .input
223 .handle_window_event(&WindowEvent::Focused(focused));
224 state
225 .game
226 .event(&mut state.ctx, &GameEvent::FocusChanged(focused));
227 }
228 WindowEvent::RedrawRequested => {
229 self.frame(event_loop);
230 }
231 ref other if !consumed => {
232 state.ctx.input.handle_window_event(other);
233 }
234 _ => {}
235 }
236 }
237
238 fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
239 if let Some(state) = self.state.as_ref() {
240 state.ctx.window.request_redraw();
241 }
242 }
243
244 fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
245 if let Some(state) = self.state.as_mut() {
246 state.game.shutdown(&mut state.ctx);
247 }
248 }
249}
250
251fn alpha(accumulator: Duration, step: Duration) -> f32 {
253 if step.is_zero() {
254 return 0.0;
255 }
256 (accumulator.as_secs_f64() / step.as_secs_f64()).clamp(0.0, 1.0) as f32
257}
258
259fn is_user_gesture(event: &WindowEvent) -> bool {
260 match event {
261 WindowEvent::KeyboardInput { event, .. } => event.state == ElementState::Pressed,
262 WindowEvent::MouseInput { state, .. } => *state == ElementState::Pressed,
263 WindowEvent::Touch(_) => true,
264 _ => false,
265 }
266}
267
268impl<G: Game> AppRunner<G> {
269 fn finish_init(
272 &mut self,
273 event_loop: &ActiveEventLoop,
274 window: Arc<Window>,
275 parts: Result<Parts>,
276 ) {
277 let parts = match parts {
278 Ok(p) => p,
279 Err(e) => {
280 log::error!("context init failed: {e:?}");
281 event_loop.exit();
282 return;
283 }
284 };
285 let mut ctx = Context::from_parts(window, parts, &self.config);
286 let size = ctx.window.inner_size();
290 ctx.gfx.resize(size.width, size.height);
291 ctx.start_audio_if_allowed();
296 let game = match G::init(&mut ctx) {
297 Ok(g) => g,
298 Err(e) => {
299 log::error!("game init failed: {e:?}");
300 event_loop.exit();
301 return;
302 }
303 };
304 ctx.window.request_redraw();
305 #[cfg(target_arch = "wasm32")]
306 let canvas_watch = {
307 let audio = ctx
308 .audio
309 .as_ref()
310 .map(game_toolkit_audio::Audio::detach_handle);
311 crate::web::CanvasWatch::new(&ctx.window, audio)
312 .inspect_err(|e| log::warn!("not watching the canvas for detach: {e:?}"))
313 .ok()
314 };
315 self.state = Some(RunState {
316 ctx,
317 game,
318 #[cfg(target_arch = "wasm32")]
319 canvas_watch,
320 });
321 }
322
323 fn frame(&mut self, event_loop: &ActiveEventLoop) {
324 let Some(state) = self.state.as_mut() else {
325 return;
326 };
327 state.ctx.time.tick();
328 state.ctx.input.poll_gamepads();
329 #[cfg(target_arch = "wasm32")]
330 state.ctx.poll_autoplay();
331 #[cfg(target_arch = "wasm32")]
332 if let Some(watch) = &state.canvas_watch {
333 for attached in watch.take_changes() {
334 if attached {
335 self.accumulator = Duration::ZERO;
337 }
338 state.ctx.canvas_attached = attached;
339 state
340 .game
341 .event(&mut state.ctx, &GameEvent::CanvasAttached(attached));
342 }
343 }
344
345 let delta = match self.config.max_frame_time {
346 Duration::ZERO => state.ctx.time.delta,
347 cap => state.ctx.time.delta.min(cap),
348 };
349 match self.config.fixed_timestep {
350 Some(step) => {
351 self.accumulator += delta;
352 let mut updated = false;
353 while self.accumulator >= step {
354 state.game.update(&mut state.ctx, step.as_secs_f32());
355 self.accumulator -= step;
356 if !updated {
359 state.ctx.input.end_frame();
360 updated = true;
361 }
362 }
363 state.ctx.time.alpha = alpha(self.accumulator, step);
364 }
365 None => {
366 state.game.update(&mut state.ctx, delta.as_secs_f32());
367 state.ctx.input.end_frame();
368 }
369 }
370
371 if state.ctx.quit_requested {
372 event_loop.exit();
373 return;
374 }
375
376 match state.ctx.gfx.begin_frame() {
377 Ok(mut frame) => {
378 state.game.render(&mut state.ctx, &mut frame);
379 state.ctx.gfx.present(frame);
380 }
381 Err(e) => {
382 log::warn!("begin_frame failed: {e:?}");
383 let (w, h) = state.ctx.gfx.size();
384 state.ctx.gfx.resize(w, h);
385 }
386 }
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn alpha_is_fraction_of_step() {
396 let step = Duration::from_millis(10);
397 assert_eq!(alpha(Duration::ZERO, step), 0.0);
398 assert!((alpha(Duration::from_millis(5), step) - 0.5).abs() < 1e-6);
399 assert_eq!(alpha(Duration::from_millis(5), Duration::ZERO), 0.0);
400 }
401}