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}
137
138impl<G: Game> ApplicationHandler<InitDone> for AppRunner<G> {
139 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
140 if self.state.is_some() {
141 return;
142 }
143 #[cfg(target_arch = "wasm32")]
144 if self.initializing {
145 return;
146 }
147 let window = match context::create_window(event_loop, &self.config) {
148 Ok(w) => w,
149 Err(e) => {
150 log::error!("window creation failed: {e:?}");
151 event_loop.exit();
152 return;
153 }
154 };
155 #[cfg(not(target_arch = "wasm32"))]
158 {
159 let parts =
160 pollster::block_on(context::init_parts(window.clone(), self.config.clone()));
161 self.finish_init(event_loop, window, parts);
162 }
163 #[cfg(target_arch = "wasm32")]
164 {
165 self.initializing = true;
166 let proxy = self.proxy.clone();
167 let config = self.config.clone();
168 wasm_bindgen_futures::spawn_local(async move {
169 let parts = context::init_parts(window.clone(), config).await;
170 if proxy.send_event(Box::new((window, parts))).is_err() {
171 log::error!("event loop closed before init finished");
172 }
173 });
174 }
175 }
176
177 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: InitDone) {
178 let (window, parts) = *event;
179 #[cfg(target_arch = "wasm32")]
180 {
181 self.initializing = false;
182 }
183 self.finish_init(event_loop, window, parts);
184 }
185
186 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
187 let Some(state) = self.state.as_mut() else {
188 return;
189 };
190 if is_user_gesture(&event) {
193 state.ctx.start_audio();
194 }
195 let consumed = state.game.raw_window_event(&mut state.ctx, &event);
197 match event {
198 WindowEvent::CloseRequested => {
199 state.game.event(&mut state.ctx, &GameEvent::CloseRequested);
200 event_loop.exit();
201 }
202 WindowEvent::Resized(size) => {
203 state.ctx.gfx.resize(size.width, size.height);
204 state.game.event(
205 &mut state.ctx,
206 &GameEvent::Resized {
207 width: size.width,
208 height: size.height,
209 },
210 );
211 }
212 WindowEvent::Focused(focused) => {
213 if focused {
214 self.accumulator = Duration::ZERO;
216 }
217 state
218 .ctx
219 .input
220 .handle_window_event(&WindowEvent::Focused(focused));
221 state
222 .game
223 .event(&mut state.ctx, &GameEvent::FocusChanged(focused));
224 }
225 WindowEvent::RedrawRequested => {
226 self.frame(event_loop);
227 }
228 ref other if !consumed => {
229 state.ctx.input.handle_window_event(other);
230 }
231 _ => {}
232 }
233 }
234
235 fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
236 if let Some(state) = self.state.as_ref() {
237 state.ctx.window.request_redraw();
238 }
239 }
240
241 fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
242 if let Some(state) = self.state.as_mut() {
243 state.game.shutdown(&mut state.ctx);
244 }
245 }
246}
247
248fn alpha(accumulator: Duration, step: Duration) -> f32 {
250 if step.is_zero() {
251 return 0.0;
252 }
253 (accumulator.as_secs_f64() / step.as_secs_f64()).clamp(0.0, 1.0) as f32
254}
255
256fn is_user_gesture(event: &WindowEvent) -> bool {
257 match event {
258 WindowEvent::KeyboardInput { event, .. } => event.state == ElementState::Pressed,
259 WindowEvent::MouseInput { state, .. } => *state == ElementState::Pressed,
260 WindowEvent::Touch(_) => true,
261 _ => false,
262 }
263}
264
265impl<G: Game> AppRunner<G> {
266 fn finish_init(
269 &mut self,
270 event_loop: &ActiveEventLoop,
271 window: Arc<Window>,
272 parts: Result<Parts>,
273 ) {
274 let parts = match parts {
275 Ok(p) => p,
276 Err(e) => {
277 log::error!("context init failed: {e:?}");
278 event_loop.exit();
279 return;
280 }
281 };
282 let mut ctx = Context::from_parts(window, parts, &self.config);
283 let size = ctx.window.inner_size();
287 ctx.gfx.resize(size.width, size.height);
288 let game = match G::init(&mut ctx) {
289 Ok(g) => g,
290 Err(e) => {
291 log::error!("game init failed: {e:?}");
292 event_loop.exit();
293 return;
294 }
295 };
296 ctx.window.request_redraw();
297 self.state = Some(RunState { ctx, game });
298 }
299
300 fn frame(&mut self, event_loop: &ActiveEventLoop) {
301 let Some(state) = self.state.as_mut() else {
302 return;
303 };
304 state.ctx.time.tick();
305 state.ctx.input.poll_gamepads();
306
307 let delta = match self.config.max_frame_time {
308 Duration::ZERO => state.ctx.time.delta,
309 cap => state.ctx.time.delta.min(cap),
310 };
311 match self.config.fixed_timestep {
312 Some(step) => {
313 self.accumulator += delta;
314 let mut updated = false;
315 while self.accumulator >= step {
316 state.game.update(&mut state.ctx, step.as_secs_f32());
317 self.accumulator -= step;
318 if !updated {
321 state.ctx.input.end_frame();
322 updated = true;
323 }
324 }
325 state.ctx.time.alpha = alpha(self.accumulator, step);
326 }
327 None => {
328 state.game.update(&mut state.ctx, delta.as_secs_f32());
329 state.ctx.input.end_frame();
330 }
331 }
332
333 if state.ctx.quit_requested {
334 event_loop.exit();
335 return;
336 }
337
338 match state.ctx.gfx.begin_frame() {
339 Ok(mut frame) => {
340 state.game.render(&mut state.ctx, &mut frame);
341 state.ctx.gfx.present(frame);
342 }
343 Err(e) => {
344 log::warn!("begin_frame failed: {e:?}");
345 let (w, h) = state.ctx.gfx.size();
346 state.ctx.gfx.resize(w, h);
347 }
348 }
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn alpha_is_fraction_of_step() {
358 let step = Duration::from_millis(10);
359 assert_eq!(alpha(Duration::ZERO, step), 0.0);
360 assert!((alpha(Duration::from_millis(5), step) - 0.5).abs() < 1e-6);
361 assert_eq!(alpha(Duration::from_millis(5), Duration::ZERO), 0.0);
362 }
363}