micro_games_kit/
game.rs

1use crate::{
2    assets::{
3        FontAssetSubsystem, ShaderAssetSubsystem, SoundAssetSubsystem, TextureAssetSubsystem,
4    },
5    audio::Audio,
6    context::GameContext,
7};
8#[cfg(not(target_arch = "wasm32"))]
9use glutin::{event::Event, window::Window};
10#[cfg(target_arch = "wasm32")]
11use instant::Instant;
12use keket::database::AssetDatabase;
13use spitfire_draw::{
14    context::DrawContext,
15    utils::{ShaderRef, Vertex},
16};
17use spitfire_glow::{app::AppState, graphics::Graphics, renderer::GlowBlending};
18use spitfire_gui::context::GuiContext;
19use spitfire_input::InputContext;
20#[cfg(not(target_arch = "wasm32"))]
21use std::time::Instant;
22#[cfg(target_arch = "wasm32")]
23use winit::{event::Event, window::Window};
24
25pub trait GameObject {
26    #[allow(unused_variables)]
27    fn activate(&mut self, context: &mut GameContext) {}
28
29    #[allow(unused_variables)]
30    fn deactivate(&mut self, context: &mut GameContext) {}
31
32    #[allow(unused_variables)]
33    fn process(&mut self, context: &mut GameContext, delta_time: f32) {}
34
35    #[allow(unused_variables)]
36    fn draw(&mut self, context: &mut GameContext) {}
37}
38
39#[derive(Default)]
40pub enum GameStateChange {
41    #[default]
42    Continue,
43    Swap(Box<dyn GameState>),
44    Push(Box<dyn GameState>),
45    Pop,
46}
47
48#[allow(unused_variables)]
49pub trait GameState {
50    fn enter(&mut self, context: GameContext) {}
51
52    fn exit(&mut self, context: GameContext) {}
53
54    fn update(&mut self, context: GameContext, delta_time: f32) {}
55
56    fn fixed_update(&mut self, context: GameContext, delta_time: f32) {}
57
58    fn draw(&mut self, context: GameContext) {}
59
60    fn draw_gui(&mut self, context: GameContext) {}
61}
62
63pub trait GameSubsystem {
64    fn run(&mut self, context: GameContext, delta_time: f32);
65}
66
67pub struct GameInstance {
68    pub fixed_delta_time: f32,
69    pub color_shader: &'static str,
70    pub image_shader: &'static str,
71    pub text_shader: &'static str,
72    pub input_maintain_on_fixed_step: bool,
73    draw: DrawContext,
74    gui: GuiContext,
75    input: InputContext,
76    assets: AssetDatabase,
77    audio: Audio,
78    timer: Instant,
79    fixed_timer: Instant,
80    states: Vec<Box<dyn GameState>>,
81    state_change: GameStateChange,
82    subsystems: Vec<Box<dyn GameSubsystem>>,
83}
84
85impl Default for GameInstance {
86    fn default() -> Self {
87        Self {
88            fixed_delta_time: 1.0 / 60.0,
89            color_shader: "color",
90            image_shader: "image",
91            text_shader: "text",
92            input_maintain_on_fixed_step: true,
93            draw: Default::default(),
94            gui: Default::default(),
95            input: Default::default(),
96            assets: Default::default(),
97            audio: Default::default(),
98            timer: Instant::now(),
99            fixed_timer: Instant::now(),
100            states: Default::default(),
101            state_change: Default::default(),
102            subsystems: vec![
103                Box::new(ShaderAssetSubsystem),
104                Box::new(TextureAssetSubsystem),
105                Box::new(FontAssetSubsystem),
106                Box::new(SoundAssetSubsystem),
107            ],
108        }
109    }
110}
111
112impl GameInstance {
113    pub fn new(state: impl GameState + 'static) -> Self {
114        Self {
115            state_change: GameStateChange::Push(Box::new(state)),
116            ..Default::default()
117        }
118    }
119
120    pub fn with_fixed_time_step(mut self, value: f32) -> Self {
121        self.fixed_delta_time = value;
122        self
123    }
124
125    pub fn with_fps(mut self, frames_per_second: usize) -> Self {
126        self.set_fps(frames_per_second);
127        self
128    }
129
130    pub fn with_color_shader(mut self, name: &'static str) -> Self {
131        self.color_shader = name;
132        self
133    }
134
135    pub fn with_image_shader(mut self, name: &'static str) -> Self {
136        self.image_shader = name;
137        self
138    }
139
140    pub fn with_text_shader(mut self, name: &'static str) -> Self {
141        self.text_shader = name;
142        self
143    }
144
145    pub fn with_input_maintain_on_fixed_step(mut self, value: bool) -> Self {
146        self.input_maintain_on_fixed_step = value;
147        self
148    }
149
150    pub fn with_subsystem(mut self, subsystem: impl GameSubsystem + 'static) -> Self {
151        self.subsystems.push(Box::new(subsystem));
152        self
153    }
154
155    pub fn setup_assets(mut self, f: impl FnOnce(&mut AssetDatabase)) -> Self {
156        f(&mut self.assets);
157        self
158    }
159
160    pub fn fps(&self) -> usize {
161        (1.0 / self.fixed_delta_time).ceil() as usize
162    }
163
164    pub fn set_fps(&mut self, frames_per_second: usize) {
165        self.fixed_delta_time = 1.0 / frames_per_second as f32;
166    }
167
168    pub fn process_frame(&mut self, graphics: &mut Graphics<Vertex>) {
169        let delta_time = self.timer.elapsed().as_secs_f32();
170
171        for subsystem in &mut self.subsystems {
172            subsystem.run(
173                GameContext {
174                    graphics,
175                    draw: &mut self.draw,
176                    gui: &mut self.gui,
177                    input: &mut self.input,
178                    state_change: &mut self.state_change,
179                    assets: &mut self.assets,
180                    audio: &mut self.audio,
181                },
182                delta_time,
183            );
184        }
185        self.assets.maintain().unwrap();
186
187        if let Some(state) = self.states.last_mut() {
188            self.timer = Instant::now();
189            state.update(
190                GameContext {
191                    graphics,
192                    draw: &mut self.draw,
193                    gui: &mut self.gui,
194                    input: &mut self.input,
195                    state_change: &mut self.state_change,
196                    assets: &mut self.assets,
197                    audio: &mut self.audio,
198                },
199                delta_time,
200            );
201        }
202
203        let fixed_delta_time = self.fixed_timer.elapsed().as_secs_f32();
204        let fixed_step = if fixed_delta_time > self.fixed_delta_time {
205            self.fixed_timer = Instant::now();
206            if let Some(state) = self.states.last_mut() {
207                state.fixed_update(
208                    GameContext {
209                        graphics,
210                        draw: &mut self.draw,
211                        gui: &mut self.gui,
212                        input: &mut self.input,
213                        state_change: &mut self.state_change,
214                        assets: &mut self.assets,
215                        audio: &mut self.audio,
216                    },
217                    fixed_delta_time,
218                );
219            }
220            true
221        } else {
222            false
223        };
224
225        self.draw.begin_frame(graphics);
226        self.draw.push_shader(&ShaderRef::name(self.image_shader));
227        self.draw.push_blending(GlowBlending::Alpha);
228        if let Some(state) = self.states.last_mut() {
229            state.draw(GameContext {
230                graphics,
231                draw: &mut self.draw,
232                gui: &mut self.gui,
233                input: &mut self.input,
234                state_change: &mut self.state_change,
235                assets: &mut self.assets,
236                audio: &mut self.audio,
237            });
238        }
239        self.gui.begin_frame();
240        if let Some(state) = self.states.last_mut() {
241            state.draw_gui(GameContext {
242                graphics,
243                draw: &mut self.draw,
244                gui: &mut self.gui,
245                input: &mut self.input,
246                state_change: &mut self.state_change,
247                assets: &mut self.assets,
248                audio: &mut self.audio,
249            });
250        }
251        self.gui.end_frame(
252            &mut self.draw,
253            graphics,
254            &ShaderRef::name(self.color_shader),
255            &ShaderRef::name(self.image_shader),
256            &ShaderRef::name(self.text_shader),
257        );
258        self.draw.end_frame();
259        if !self.input_maintain_on_fixed_step || fixed_step {
260            self.input.maintain();
261        }
262
263        match std::mem::take(&mut self.state_change) {
264            GameStateChange::Continue => {}
265            GameStateChange::Swap(mut state) => {
266                if let Some(mut state) = self.states.pop() {
267                    state.exit(GameContext {
268                        graphics,
269                        draw: &mut self.draw,
270                        gui: &mut self.gui,
271                        input: &mut self.input,
272                        state_change: &mut self.state_change,
273                        assets: &mut self.assets,
274                        audio: &mut self.audio,
275                    });
276                }
277                state.enter(GameContext {
278                    graphics,
279                    draw: &mut self.draw,
280                    gui: &mut self.gui,
281                    input: &mut self.input,
282                    state_change: &mut self.state_change,
283                    assets: &mut self.assets,
284                    audio: &mut self.audio,
285                });
286                self.states.push(state);
287                self.timer = Instant::now();
288            }
289            GameStateChange::Push(mut state) => {
290                state.enter(GameContext {
291                    graphics,
292                    draw: &mut self.draw,
293                    gui: &mut self.gui,
294                    input: &mut self.input,
295                    state_change: &mut self.state_change,
296                    assets: &mut self.assets,
297                    audio: &mut self.audio,
298                });
299                self.states.push(state);
300                self.timer = Instant::now();
301            }
302            GameStateChange::Pop => {
303                if let Some(mut state) = self.states.pop() {
304                    state.exit(GameContext {
305                        graphics,
306                        draw: &mut self.draw,
307                        gui: &mut self.gui,
308                        input: &mut self.input,
309                        state_change: &mut self.state_change,
310                        assets: &mut self.assets,
311                        audio: &mut self.audio,
312                    });
313                }
314                self.timer = Instant::now();
315            }
316        }
317    }
318
319    pub fn process_event(&mut self, event: &Event<()>) -> bool {
320        if let Event::WindowEvent { event, .. } = event {
321            self.input.on_event(event);
322        }
323        !self.states.is_empty() || !matches!(self.state_change, GameStateChange::Continue)
324    }
325}
326
327impl AppState<Vertex> for GameInstance {
328    fn on_redraw(&mut self, graphics: &mut Graphics<Vertex>) {
329        self.process_frame(graphics);
330    }
331
332    fn on_event(&mut self, event: Event<()>, _: &mut Window) -> bool {
333        self.process_event(&event)
334    }
335}