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>,
25 pub asset_root: std::path::PathBuf,
28 pub web_preload: Option<Vec<String>>,
30 pub canvas_id: Option<String>,
33 pub random_seed: u64,
35 pub depth_format: Option<wgpu::TextureFormat>,
39 pub msaa_samples: u32,
42}
43
44impl Default for AppConfig {
45 fn default() -> Self {
46 Self {
47 title: "Game".into(),
48 width: 1280,
49 height: 720,
50 vsync: true,
51 fixed_timestep: None,
52 asset_root: std::path::PathBuf::from("assets"),
53 web_preload: None,
54 canvas_id: None,
55 random_seed: crate::Rng::DEFAULT_SEED,
56 depth_format: None,
57 msaa_samples: 1,
58 }
59 }
60}
61
62type InitDone = Box<(Arc<Window>, Result<Parts>)>;
65
66pub fn run<G: Game>(config: AppConfig) -> Result<()> {
72 #[cfg(target_arch = "wasm32")]
73 crate::web::install_hooks();
74 let event_loop = EventLoop::<InitDone>::with_user_event().build()?;
75 event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
76 #[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
77 let mut runner = AppRunner::<G> {
78 config,
79 state: None,
80 accumulator: Duration::ZERO,
81 #[cfg(target_arch = "wasm32")]
82 proxy: event_loop.create_proxy(),
83 #[cfg(target_arch = "wasm32")]
84 initializing: false,
85 };
86 #[cfg(not(target_arch = "wasm32"))]
87 event_loop.run_app(&mut runner)?;
88 #[cfg(target_arch = "wasm32")]
89 {
90 use winit::platform::web::EventLoopExtWebSys;
91 event_loop.spawn_app(runner);
92 }
93 Ok(())
94}
95
96struct AppRunner<G: Game> {
97 config: AppConfig,
98 state: Option<RunState<G>>,
99 accumulator: Duration,
100 #[cfg(target_arch = "wasm32")]
101 proxy: winit::event_loop::EventLoopProxy<InitDone>,
102 #[cfg(target_arch = "wasm32")]
104 initializing: bool,
105}
106
107struct RunState<G: Game> {
108 ctx: Context,
109 game: G,
110}
111
112impl<G: Game> ApplicationHandler<InitDone> for AppRunner<G> {
113 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
114 if self.state.is_some() {
115 return;
116 }
117 #[cfg(target_arch = "wasm32")]
118 if self.initializing {
119 return;
120 }
121 let window = match context::create_window(event_loop, &self.config) {
122 Ok(w) => w,
123 Err(e) => {
124 log::error!("window creation failed: {e:?}");
125 event_loop.exit();
126 return;
127 }
128 };
129 #[cfg(not(target_arch = "wasm32"))]
132 {
133 let parts =
134 pollster::block_on(context::init_parts(window.clone(), self.config.clone()));
135 self.finish_init(event_loop, window, parts);
136 }
137 #[cfg(target_arch = "wasm32")]
138 {
139 self.initializing = true;
140 let proxy = self.proxy.clone();
141 let config = self.config.clone();
142 wasm_bindgen_futures::spawn_local(async move {
143 let parts = context::init_parts(window.clone(), config).await;
144 if proxy.send_event(Box::new((window, parts))).is_err() {
145 log::error!("event loop closed before init finished");
146 }
147 });
148 }
149 }
150
151 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: InitDone) {
152 let (window, parts) = *event;
153 #[cfg(target_arch = "wasm32")]
154 {
155 self.initializing = false;
156 }
157 self.finish_init(event_loop, window, parts);
158 }
159
160 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
161 let Some(state) = self.state.as_mut() else {
162 return;
163 };
164 if is_user_gesture(&event) {
167 state.ctx.start_audio();
168 }
169 let consumed = state.game.raw_window_event(&mut state.ctx, &event);
171 match event {
172 WindowEvent::CloseRequested => {
173 state.game.event(&mut state.ctx, &GameEvent::CloseRequested);
174 event_loop.exit();
175 }
176 WindowEvent::Resized(size) => {
177 state.ctx.gfx.resize(size.width, size.height);
178 state.game.event(
179 &mut state.ctx,
180 &GameEvent::Resized {
181 width: size.width,
182 height: size.height,
183 },
184 );
185 }
186 WindowEvent::Focused(focused) => {
187 state
188 .ctx
189 .input
190 .handle_window_event(&WindowEvent::Focused(focused));
191 state
192 .game
193 .event(&mut state.ctx, &GameEvent::FocusChanged(focused));
194 }
195 WindowEvent::RedrawRequested => {
196 self.frame(event_loop);
197 }
198 ref other if !consumed => {
199 state.ctx.input.handle_window_event(other);
200 }
201 _ => {}
202 }
203 }
204
205 fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
206 if let Some(state) = self.state.as_ref() {
207 state.ctx.window.request_redraw();
208 }
209 }
210
211 fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
212 if let Some(state) = self.state.as_mut() {
213 state.game.shutdown(&mut state.ctx);
214 }
215 }
216}
217
218fn is_user_gesture(event: &WindowEvent) -> bool {
219 match event {
220 WindowEvent::KeyboardInput { event, .. } => event.state == ElementState::Pressed,
221 WindowEvent::MouseInput { state, .. } => *state == ElementState::Pressed,
222 WindowEvent::Touch(_) => true,
223 _ => false,
224 }
225}
226
227impl<G: Game> AppRunner<G> {
228 fn finish_init(
231 &mut self,
232 event_loop: &ActiveEventLoop,
233 window: Arc<Window>,
234 parts: Result<Parts>,
235 ) {
236 let parts = match parts {
237 Ok(p) => p,
238 Err(e) => {
239 log::error!("context init failed: {e:?}");
240 event_loop.exit();
241 return;
242 }
243 };
244 let mut ctx = Context::from_parts(window, parts, &self.config);
245 let size = ctx.window.inner_size();
249 ctx.gfx.resize(size.width, size.height);
250 let game = match G::init(&mut ctx) {
251 Ok(g) => g,
252 Err(e) => {
253 log::error!("game init failed: {e:?}");
254 event_loop.exit();
255 return;
256 }
257 };
258 ctx.window.request_redraw();
259 self.state = Some(RunState { ctx, game });
260 }
261
262 fn frame(&mut self, event_loop: &ActiveEventLoop) {
263 let Some(state) = self.state.as_mut() else {
264 return;
265 };
266 state.ctx.time.tick();
267 state.ctx.input.poll_gamepads();
268
269 match self.config.fixed_timestep {
270 Some(step) => {
271 self.accumulator += state.ctx.time.delta;
272 while self.accumulator >= step {
273 state.game.update(&mut state.ctx, step.as_secs_f32());
274 self.accumulator -= step;
275 }
276 }
277 None => {
278 let dt = state.ctx.time.delta.as_secs_f32();
279 state.game.update(&mut state.ctx, dt);
280 }
281 }
282
283 if state.ctx.quit_requested {
284 event_loop.exit();
285 return;
286 }
287
288 state.ctx.input.end_frame();
289
290 match state.ctx.gfx.begin_frame() {
291 Ok(mut frame) => {
292 state.game.render(&mut state.ctx, &mut frame);
293 state.ctx.gfx.present(frame);
294 }
295 Err(e) => {
296 log::warn!("begin_frame failed: {e:?}");
297 let (w, h) = state.ctx.gfx.size();
298 state.ctx.gfx.resize(w, h);
299 }
300 }
301 }
302}