Skip to main content

mireforge_game/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/mireforge/mireforge
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5extern crate core;
6
7pub mod prelude;
8
9use int_math::{URect, UVec2, Vec2};
10
11use fixed32::Fp;
12use limnus_app::prelude::{App, AppReturnValue, ApplicationExit, Plugin};
13use limnus_audio_mixer::{AudioMixer, StereoSample};
14use limnus_basic_input::InputMessage;
15use limnus_basic_input::prelude::{
16    ButtonState, KeyCode, MouseButton, MouseScrollDelta, TouchPhase,
17};
18use limnus_default_stages::{FixedUpdate, RenderUpdate, Update};
19use limnus_gamepad::{Axis, Button, GamePadId, Gamepad, GamepadMessage, Gamepads};
20use limnus_local_resource::prelude::LocalResource;
21use limnus_message::MessagesIterator;
22use limnus_resource::ResourceStorage;
23use limnus_resource::prelude::Resource;
24use limnus_screen::WindowMessage;
25use limnus_system_params::{LoReM, Msg, Re, ReAll, ReM};
26use mireforge_game_assets::{Assets, GameAssets};
27use mireforge_game_audio::{Audio, GameAudio};
28use mireforge_render_wgpu::prelude::{Gfx, Render};
29use monotonic_time_rs::{InstantMonotonicClock, Millis, MonotonicClock};
30use std::cmp::{max, min};
31use std::fmt::{Debug, Formatter};
32use std::marker::PhantomData;
33use tracing::debug;
34
35pub trait Application: Sized + 'static {
36    fn new(assets: &mut impl Assets) -> Self;
37    fn tick(&mut self, assets: &mut impl Assets);
38    fn render(&mut self, gfx: &mut impl Gfx);
39    fn audio(&mut self, _audio: &mut impl Audio) {}
40
41    fn wants_to_quit(&self) -> bool {
42        false
43    }
44
45    fn wants_cursor_visible(&self) -> bool {
46        true
47    }
48
49    fn keyboard_input(&mut self, _state: ButtonState, _key_code: KeyCode) {}
50
51    fn cursor_entered(&mut self) {}
52
53    fn cursor_left(&mut self) {}
54
55    fn cursor_moved(&mut self, _position: UVec2) {}
56
57    fn touch(&mut self, _position: UVec2, _touch_phase: &TouchPhase) {}
58
59    fn mouse_input(&mut self, _state: ButtonState, _button: MouseButton) {}
60
61    fn mouse_wheel(&mut self, _delta_y: i16) {}
62
63    fn mouse_motion(&mut self, _delta: Vec2) {}
64
65    fn gamepad_activated(&mut self, _gamepad_id: GamePadId, _name: String) {}
66    fn gamepad_button_changed(&mut self, _gamepad: &Gamepad, _button: Button, _value: Fp) {}
67    fn gamepad_axis_changed(&mut self, _gamepad: &Gamepad, _axis: Axis, _value: Fp) {}
68    fn gamepad_disconnected(&mut self, _gamepad_id: GamePadId) {}
69
70    fn scale_factor_changed(&mut self, _scale_factor: f64) -> Option<UVec2> {
71        None
72    }
73}
74
75#[derive(Debug, Resource)]
76pub struct GameSettings {
77    pub virtual_size: UVec2,
78}
79
80#[derive(LocalResource)]
81pub struct Game<G: Application> {
82    game: G,
83    clock: InstantMonotonicClock,
84}
85
86impl<G: Application> Debug for Game<G> {
87    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
88        write!(f, "WgpuGame")
89    }
90}
91
92impl<G: Application> Game<G> {
93    #[must_use]
94    pub fn new(all_resources: &mut ResourceStorage) -> Self {
95        let clock = InstantMonotonicClock::new();
96        let mut assets = GameAssets::new(all_resources, clock.now());
97        let game = G::new(&mut assets);
98
99        Self { game, clock }
100    }
101
102    pub fn inputs(&mut self, iter: MessagesIterator<InputMessage>) {
103        for message in iter {
104            match message {
105                InputMessage::KeyboardInput(button_state, key_code) => {
106                    self.game.keyboard_input(*button_state, *key_code);
107                }
108                InputMessage::MouseInput(button_state, button) => {
109                    self.game.mouse_input(*button_state, *button);
110                }
111                InputMessage::MouseWheel(scroll_delta, _touch_phase) => {
112                    if let MouseScrollDelta::LineDelta(delta) = scroll_delta {
113                        let game_scroll_y = (f32::from(-delta.y) * 120.0) as i16;
114                        self.game.mouse_wheel(game_scroll_y);
115                    }
116                }
117            }
118        }
119    }
120
121    #[must_use]
122    pub fn virtual_position_from_physical(
123        physical_position: UVec2,
124        viewport: URect,
125        virtual_surface_size: UVec2,
126    ) -> UVec2 {
127        let relative_x = max(
128            0,
129            min(
130                i64::from(physical_position.x) - i64::from(viewport.position.x),
131                i64::from(viewport.size.x - 1),
132            ),
133        );
134
135        let relative_y = max(
136            0,
137            min(
138                i64::from(physical_position.y) - i64::from(viewport.position.y),
139                i64::from(viewport.size.y - 1),
140            ),
141        );
142
143        let clamped_to_viewport: UVec2 = UVec2::new(relative_x as u16, relative_y as u16);
144
145        let virtual_position_x = (u64::from(clamped_to_viewport.x)
146            * u64::from(virtual_surface_size.x))
147            / u64::from(viewport.size.x);
148
149        let virtual_position_y = (u64::from(clamped_to_viewport.y)
150            * u64::from(virtual_surface_size.y))
151            / u64::from(viewport.size.y);
152
153        UVec2::new(virtual_position_x as u16, virtual_position_y as u16)
154    }
155
156    pub fn cursor_moved(
157        &mut self,
158        physical_position: UVec2,
159        viewport: URect,
160        virtual_surface_size: UVec2,
161    ) {
162        let virtual_position =
163            Self::virtual_position_from_physical(physical_position, viewport, virtual_surface_size);
164        self.game.cursor_moved(virtual_position);
165    }
166
167    pub fn touch(
168        &mut self,
169        physical_position: UVec2,
170        touch_phase: &TouchPhase,
171        viewport: URect,
172        virtual_surface_size: UVec2,
173    ) {
174        let virtual_position =
175            Self::virtual_position_from_physical(physical_position, viewport, virtual_surface_size);
176        self.game.touch(virtual_position, touch_phase);
177    }
178
179    pub fn mouse_move(&mut self, iter: MessagesIterator<WindowMessage>, wgpu_render: &Render) {
180        for message in iter {
181            match message {
182                WindowMessage::CursorMoved(position) => self.cursor_moved(
183                    *position,
184                    wgpu_render.viewport(),
185                    wgpu_render.virtual_surface_size_with_scaling(),
186                ),
187                WindowMessage::Touch(position, touch_phase) => self.touch(
188                    *position,
189                    touch_phase,
190                    wgpu_render.viewport(),
191                    wgpu_render.virtual_surface_size_with_scaling(),
192                ),
193                WindowMessage::WindowCreated() => {}
194                WindowMessage::Resized(_) => {}
195            }
196        }
197    }
198
199    pub fn tick(&mut self, storage: &mut ResourceStorage, now: Millis) {
200        // This is a quick operation, we basically wrap storage
201        let mut assets = GameAssets::new(storage, now);
202
203        self.game.tick(&mut assets);
204    }
205
206    pub fn render(&mut self, wgpu_render: &mut Render, now: Millis) {
207        wgpu_render.set_now(now);
208        self.game.render(wgpu_render);
209    }
210}
211
212pub struct GamePlugin<G: Application> {
213    pub phantom_data: PhantomData<G>,
214}
215impl<G: Application> Default for GamePlugin<G> {
216    fn default() -> Self {
217        Self::new()
218    }
219}
220
221impl<G: Application> GamePlugin<G> {
222    #[must_use]
223    pub const fn new() -> Self {
224        Self {
225            phantom_data: PhantomData,
226        }
227    }
228}
229
230pub fn mouse_input_tick<G: Application>(
231    mut internal_game: LoReM<Game<G>>,
232    window_messages: Msg<WindowMessage>,
233    wgpu_render: Re<Render>,
234) {
235    internal_game.mouse_move(window_messages.iter_previous(), &wgpu_render);
236}
237
238pub fn keyboard_input_tick<G: Application>(
239    mut internal_game: LoReM<Game<G>>,
240    input_messages: Msg<InputMessage>,
241) {
242    internal_game.inputs(input_messages.iter_previous());
243}
244
245pub fn audio_tick<G: Application>(
246    mut internal_game: LoReM<Game<G>>,
247    stereo_samples: Re<limnus_assets::Assets<StereoSample>>,
248    mut audio_mixer: LoReM<AudioMixer>,
249) {
250    let mut game_audio = GameAudio::new(&mut audio_mixer, &stereo_samples);
251    internal_game.game.audio(&mut game_audio);
252}
253
254pub fn logic_tick<G: Application>(mut internal_game: LoReM<Game<G>>, mut all_resources: ReAll) {
255    let now = internal_game.clock.now();
256
257    internal_game.tick(&mut all_resources, now);
258    if internal_game.game.wants_to_quit() {
259        all_resources.insert(ApplicationExit {
260            value: AppReturnValue::Value(0),
261        });
262    }
263}
264
265pub fn render_tick<G: Application>(
266    mut internal_game: LoReM<Game<G>>,
267    mut wgpu_render: ReM<Render>,
268) {
269    let now = internal_game.clock.now();
270
271    internal_game.render(&mut wgpu_render, now);
272}
273
274pub fn gamepad_input_tick<G: Application>(
275    mut internal_game: LoReM<Game<G>>,
276    gamepads: Re<Gamepads>,
277    gamepad_messages: Msg<GamepadMessage>,
278) {
279    for gamepad_message in gamepad_messages.iter_current() {
280        match gamepad_message {
281            GamepadMessage::Connected(_gamepad_id, _gamepad_name) => {}
282            GamepadMessage::Disconnected(gamepad_id) => {
283                if let Some(gamepad) = gamepads.gamepad(*gamepad_id)
284                    && gamepad.is_active
285                {
286                    internal_game.game.gamepad_disconnected(*gamepad_id);
287                }
288            }
289            GamepadMessage::Activated(gamepad_id) => {
290                if let Some(gamepad) = gamepads.gamepad(*gamepad_id) {
291                    internal_game
292                        .game
293                        .gamepad_activated(*gamepad_id, gamepad.name.as_str().to_string());
294                }
295            }
296            GamepadMessage::ButtonChanged(gamepad_id, button, value) => {
297                if let Some(gamepad) = gamepads.gamepad(*gamepad_id)
298                    && gamepad.is_active
299                {
300                    internal_game
301                        .game
302                        .gamepad_button_changed(gamepad, *button, Fp::from(*value));
303                }
304            }
305            GamepadMessage::AxisChanged(gamepad_id, axis, value) => {
306                if let Some(gamepad) = gamepads.gamepad(*gamepad_id)
307                    && gamepad.is_active
308                {
309                    internal_game
310                        .game
311                        .gamepad_axis_changed(gamepad, *axis, Fp::from(*value));
312                }
313            }
314        }
315    }
316}
317
318impl<G: Application> Plugin for GamePlugin<G> {
319    fn post_initialization(&self, app: &mut App) {
320        debug!("calling WgpuGame::new()");
321
322        let all_resources = app.resources_mut();
323        let internal_game = Game::<G>::new(all_resources);
324        app.insert_local_resource(internal_game);
325
326        app.add_system(Update, gamepad_input_tick::<G>);
327        app.add_system(Update, keyboard_input_tick::<G>);
328        app.add_system(Update, mouse_input_tick::<G>);
329        app.add_system(Update, audio_tick::<G>);
330        app.add_system(FixedUpdate, logic_tick::<G>);
331        app.add_system(RenderUpdate, render_tick::<G>);
332    }
333}