Skip to main content

game_gem/
engine.rs

1//! Engine core: Context, game loop, and window configuration.
2//!
3//! ## Why game-gem > macroquad (architecture)
4//!
5//! | Feature               | macroquad                  | game-gem                            |
6//! |-----------------------|----------------------------|-------------------------------------|
7//! | Entry point           | `#[macroquad::main]` proc macro | Plain `fn main()` + `Game::run()`  |
8//! | State management      | Global variables           | Trait-based `GameState`              |
9//! | Error handling        | Panics                     | `Result` types                      |
10//! | Context passing       | Global functions           | Explicit `&mut Context` parameter    |
11//! | Modularity            | All-or-nothing             | Feature-gated modules               |
12//! | Multiple windows      | No                         | Planned                             |
13//! | Custom game loop      | No                         | Fixed timestep, variable, or custom |
14//!
15//! ## Minimal example
16//!
17//! ```no_run
18//! use game_gem::prelude::*;
19//!
20//! struct MyGame;
21//!
22//! impl GameState for MyGame {
23//!     fn update(&mut self, ctx: &mut Context) {
24//!         if ctx.input.keyboard.is_pressed(KeyCode::Escape) {
25//!             ctx.quit();
26//!         }
27//!     }
28//!     fn render(&mut self, ctx: &mut Context) {
29//!         ctx.graphics.clear(Color::SKY_BLUE);
30//!         ctx.graphics.draw_circle(ctx, 400.0, 300.0, 50.0, Color::RED);
31//!     }
32//! }
33//!
34//! fn main() {
35//!     Game::new()
36//!         .window_title("My Game")
37//!         .window_size(800, 600)
38//!         .run(MyGame);
39//! }
40//! ```
41
42use crate::time::Time;
43use crate::input::InputState;
44use crate::camera::Camera;
45use crate::color::Color;
46use crate::math::Vec2;
47
48#[cfg(feature = "audio")]
49use crate::audio::AudioManager;
50#[cfg(feature = "ui")]
51use crate::ui::{UiState, UiTheme};
52#[cfg(feature = "particles")]
53use crate::particles::ParticleEmitter;
54#[cfg(feature = "tween")]
55use crate::tween::TweenManager;
56
57// ─────────────────────────────────────────────
58// Window configuration
59// ─────────────────────────────────────────────
60
61/// Window configuration builder.
62///
63/// Use `Game::new()` to create one, then chain methods to configure.
64#[derive(Debug, Clone)]
65pub struct WindowConfig {
66    /// Window title.
67    pub title: String,
68    /// Window width in pixels.
69    pub width: u32,
70    /// Window height in pixels.
71    pub height: u32,
72    /// Whether the window is resizable.
73    pub resizable: bool,
74    /// Whether to start in fullscreen.
75    pub fullscreen: bool,
76    /// Whether to show the window (can start hidden).
77    pub visible: bool,
78    /// Minimum window size.
79    pub min_size: Option<(u32, u32)>,
80    /// Maximum window size.
81    pub max_size: Option<(u32, u32)>,
82    /// Whether to enable vsync.
83    pub vsync: bool,
84    /// MSAA sample count (0 = disabled).
85    pub msaa_samples: u32,
86    /// High-DPI / retina support.
87    pub high_dpi: bool,
88    /// Icon path (set after window creation).
89    pub icon_path: Option<String>,
90    /// Target FPS cap (0 = unlimited).
91    pub target_fps: u32,
92    /// Background color when the window is cleared.
93    pub clear_color: Color,
94}
95
96impl Default for WindowConfig {
97    fn default() -> Self {
98        Self {
99            title: "game-gem".to_string(),
100            width: 800,
101            height: 600,
102            resizable: true,
103            fullscreen: false,
104            visible: true,
105            min_size: None,
106            max_size: None,
107            vsync: true,
108            msaa_samples: 0,
109            high_dpi: true,
110            icon_path: None,
111            target_fps: 0,
112            clear_color: Color::from_hex("#1A1A2E").unwrap(),
113        }
114    }
115}
116
117// ─────────────────────────────────────────────
118// Graphics context (drawing commands stored here)
119// ─────────────────────────────────────────────
120
121/// Drawing operations that the game can issue each frame.
122///
123/// In the real rendering backend, these would be batched and sent to the GPU.
124/// For the API definition, we store the draw commands for deferred rendering.
125#[derive(Debug, Clone)]
126pub enum DrawCommand {
127    Clear { color: Color },
128    DrawCircle { x: f32, y: f32, radius: f32, color: Color },
129    DrawRect { x: f32, y: f32, w: f32, h: f32, color: Color },
130    DrawLine { x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32, color: Color },
131    DrawText { text: String, x: f32, y: f32, size: f32, color: Color },
132    SetCamera { camera: Camera },
133    DrawTriangle { x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32, color: Color },
134    DrawPoly { points: Vec<Vec2>, color: Color },
135    DrawEllipse { x: f32, y: f32, rx: f32, ry: f32, color: Color },
136    DrawRing { x: f32, y: f32, inner_radius: f32, outer_radius: f32, color: Color },
137    DrawArc { x: f32, y: f32, radius: f32, start_angle: f32, end_angle: f32, color: Color },
138}
139
140/// Graphics context that collects draw commands.
141///
142/// In a full implementation, this wraps the miniquad rendering pipeline
143/// with batched rendering, GPU resource management, and shader programs.
144pub struct GraphicsContext {
145    /// Draw commands for the current frame.
146    commands: Vec<DrawCommand>,
147    /// Current camera.
148    pub camera: Camera,
149    /// Default camera.
150    default_camera: Camera,
151    /// Screen size in pixels.
152    pub screen_size: Vec2,
153    /// DPI scale factor.
154    pub dpi_scale: f32,
155}
156
157impl GraphicsContext {
158    fn new(width: u32, height: u32) -> Self {
159        let screen_size = Vec2::new(width as f32, height as f32);
160        let default_camera = Camera::centered();
161        Self {
162            commands: Vec::with_capacity(1024),
163            camera: default_camera.clone(),
164            default_camera,
165            screen_size,
166            dpi_scale: 1.0,
167        }
168    }
169
170    /// Clear the screen with a color.
171    pub fn clear(&mut self, color: Color) {
172        self.commands.push(DrawCommand::Clear { color });
173    }
174
175    /// Draw a filled circle.
176    pub fn draw_circle(&mut self, x: f32, y: f32, radius: f32, color: Color) {
177        self.commands.push(DrawCommand::DrawCircle { x, y, radius, color });
178    }
179
180    /// Draw a filled rectangle.
181    pub fn draw_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Color) {
182        self.commands.push(DrawCommand::DrawRect { x, y, w, h, color });
183    }
184
185    /// Draw a line segment.
186    pub fn draw_line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32, color: Color) {
187        self.commands.push(DrawCommand::DrawLine { x1, y1, x2, y2, thickness, color });
188    }
189
190    /// Draw text (placeholder — real impl uses font rendering).
191    pub fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: Color) {
192        self.commands.push(DrawCommand::DrawText {
193            text: text.to_string(),
194            x, y, size, color,
195        });
196    }
197
198    /// Draw a filled triangle.
199    pub fn draw_triangle(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32, color: Color) {
200        self.commands.push(DrawCommand::DrawTriangle { x1, y1, x2, y2, x3, y3, color });
201    }
202
203    /// Draw a filled polygon.
204    pub fn draw_poly(&mut self, points: Vec<Vec2>, color: Color) {
205        self.commands.push(DrawCommand::DrawPoly { points, color });
206    }
207
208    /// Draw a filled ellipse.
209    pub fn draw_ellipse(&mut self, x: f32, y: f32, rx: f32, ry: f32, color: Color) {
210        self.commands.push(DrawCommand::DrawEllipse { x, y, rx, ry, color });
211    }
212
213    /// Draw a ring (annulus).
214    pub fn draw_ring(&mut self, x: f32, y: f32, inner_radius: f32, outer_radius: f32, color: Color) {
215        self.commands.push(DrawCommand::DrawRing { x, y, inner_radius, outer_radius, color });
216    }
217
218    /// Draw an arc.
219    pub fn draw_arc(&mut self, x: f32, y: f32, radius: f32, start_angle: f32, end_angle: f32, color: Color) {
220        self.commands.push(DrawCommand::DrawArc { x, y, radius, start_angle, end_angle, color });
221    }
222
223    /// Set the active camera for subsequent draw calls.
224    pub fn set_camera(&mut self, camera: Camera) {
225        self.commands.push(DrawCommand::SetCamera { camera });
226    }
227
228    /// Reset to the default camera.
229    pub fn reset_camera(&mut self) {
230        self.commands.push(DrawCommand::SetCamera {
231            camera: self.default_camera.clone(),
232        });
233    }
234
235    /// Flush all commands (called at the end of each frame by the engine).
236    fn flush(&mut self) -> Vec<DrawCommand> {
237        std::mem::take(&mut self.commands)
238    }
239}
240
241// ─────────────────────────────────────────────
242// Context (passed to all game state methods)
243// ─────────────────────────────────────────────
244
245/// The main context object passed to all game state methods.
246///
247/// Contains all engine subsystems: time, input, graphics, audio, etc.
248/// This is the single point of access for everything the game needs each frame.
249pub struct Context {
250    /// Time and delta tracking.
251    pub time: Time,
252    /// Input state (keyboard, mouse, actions).
253    pub input: InputState,
254    /// Graphics context (drawing commands).
255    pub graphics: GraphicsContext,
256    /// Audio manager (feature-gated).
257    #[cfg(feature = "audio")]
258    pub audio: AudioManager,
259    /// UI state and theme (feature-gated).
260    #[cfg(feature = "ui")]
261    pub ui_state: UiState,
262    #[cfg(feature = "ui")]
263    pub ui_theme: UiTheme,
264    /// Active particle emitters (feature-gated).
265    #[cfg(feature = "particles")]
266    pub particles: Vec<ParticleEmitter>,
267    /// Active tweens (feature-gated).
268    #[cfg(feature = "tween")]
269    pub tweens: TweenManager,
270    /// Window configuration.
271    pub window: WindowConfig,
272    /// Whether the game should exit.
273    should_quit: bool,
274    /// Whether the game is currently paused (time still advances but update is skipped).
275    paused: bool,
276}
277
278impl Default for Context {
279    fn default() -> Self {
280        Self::new(WindowConfig::default())
281    }
282}
283
284impl Context {
285    /// Create a new context from window configuration.
286    pub(crate) fn new(config: WindowConfig) -> Self {
287        let graphics = GraphicsContext::new(config.width, config.height);
288        Self {
289            time: Time::new(),
290            input: InputState::default(),
291            graphics,
292            #[cfg(feature = "audio")]
293            audio: AudioManager::new(),
294            #[cfg(feature = "ui")]
295            ui_state: UiState::default(),
296            #[cfg(feature = "ui")]
297            ui_theme: UiTheme::default(),
298            #[cfg(feature = "particles")]
299            particles: Vec::new(),
300            #[cfg(feature = "tween")]
301            tweens: TweenManager::new(),
302            window: config,
303            should_quit: false,
304            paused: false,
305        }
306    }
307
308    /// Request the game to exit (will close after the current frame).
309    pub fn quit(&mut self) {
310        self.should_quit = true;
311    }
312
313    /// Check if the game is about to exit.
314    pub fn is_quitting(&self) -> bool {
315        self.should_quit
316    }
317
318    /// Pause the game (skips update but still renders).
319    pub fn pause(&mut self) {
320        self.paused = true;
321    }
322
323    /// Resume from pause.
324    pub fn resume(&mut self) {
325        self.paused = false;
326    }
327
328    /// Check if the game is paused.
329    pub fn is_paused(&self) -> bool {
330        self.paused
331    }
332
333    /// Toggle pause.
334    pub fn toggle_pause(&mut self) {
335        self.paused = !self.paused;
336    }
337
338    /// Current screen width.
339    pub fn screen_width(&self) -> f32 {
340        self.graphics.screen_size.x
341    }
342
343    /// Current screen height.
344    pub fn screen_height(&self) -> f32 {
345        self.graphics.screen_size.y
346    }
347
348    /// Current screen size as Vec2.
349    pub fn screen_size(&self) -> Vec2 {
350        self.graphics.screen_size
351    }
352
353    /// Handle window resize.
354    pub fn handle_resize(&mut self, width: u32, height: u32) {
355        self.graphics.screen_size = Vec2::new(width as f32, height as f32);
356        self.window.width = width;
357        self.window.height = height;
358    }
359}
360
361// ─────────────────────────────────────────────
362// GameState trait
363// ─────────────────────────────────────────────
364
365/// The main game state trait. Implement this for your game.
366///
367/// The engine calls these methods every frame in order:
368/// 1. [`on_enter`](GameState::on_enter) (once, when the game starts)
369/// 2. [`fixed_update`](GameState::fixed_update) (zero or more times, if fixed timestep is enabled)
370/// 3. [`update`](GameState::update) (once per frame)
371/// 4. [`render`](GameState::render) (once per frame)
372///
373/// To add scene management, use [`crate::scene::SceneManager`] inside your game state.
374pub trait GameState {
375    /// Called once when the game starts (after window creation).
376    fn on_enter(&mut self, _ctx: &mut Context) {}
377
378    /// Called when the game is about to exit.
379    fn on_exit(&mut self, _ctx: &mut Context) {}
380
381    /// Fixed-rate update (for physics, networking, etc.).
382    /// Called zero or more times per frame at the fixed timestep rate.
383    fn fixed_update(&mut self, _ctx: &mut Context) {}
384
385    /// Main update logic. Called once per frame.
386    fn update(&mut self, ctx: &mut Context);
387
388    /// Render the game. Called once per frame, after update.
389    fn render(&mut self, ctx: &mut Context);
390}
391
392// ─────────────────────────────────────────────
393// Game builder
394// ─────────────────────────────────────────────
395
396/// The game builder. Configure your game and run it.
397///
398/// # Example
399/// ```no_run
400/// use game_gem::prelude::*;
401///
402/// struct MyGame;
403/// impl GameState for MyGame {
404///     fn update(&mut self, ctx: &mut Context) {
405///         if ctx.input.keyboard.is_pressed(KeyCode::Escape) { ctx.quit(); }
406///     }
407///     fn render(&mut self, ctx: &mut Context) {
408///         ctx.graphics.clear(Color::BLACK);
409///     }
410/// }
411///
412/// fn main() {
413///     Game::new()
414///         .window_title("Hello game-gem!")
415///         .window_size(1024, 768)
416///         .clear_color(Color::from_hex("#1A1A2E").unwrap())
417///         .target_fps(60)
418///         .run(MyGame);
419/// }
420/// ```
421pub struct Game {
422    config: WindowConfig,
423}
424
425impl Game {
426    /// Create a new game with default configuration.
427    pub fn new() -> Self {
428        Self {
429            config: WindowConfig::default(),
430        }
431    }
432
433    /// Set the window title.
434    pub fn window_title(mut self, title: &str) -> Self {
435        self.config.title = title.to_string();
436        self
437    }
438
439    /// Set the window size in pixels.
440    pub fn window_size(mut self, width: u32, height: u32) -> Self {
441        self.config.width = width;
442        self.config.height = height;
443        self
444    }
445
446    /// Set whether the window is resizable.
447    pub fn resizable(mut self, resizable: bool) -> Self {
448        self.config.resizable = resizable;
449        self
450    }
451
452    /// Set whether to start in fullscreen.
453    pub fn fullscreen(mut self, fullscreen: bool) -> Self {
454        self.config.fullscreen = fullscreen;
455        self
456    }
457
458    /// Set whether to enable vsync.
459    pub fn vsync(mut self, vsync: bool) -> Self {
460        self.config.vsync = vsync;
461        self
462    }
463
464    /// Set MSAA samples (0 = disabled).
465    pub fn msaa(mut self, samples: u32) -> Self {
466        self.config.msaa_samples = samples;
467        self
468    }
469
470    /// Set the clear/background color.
471    pub fn clear_color(mut self, color: Color) -> Self {
472        self.config.clear_color = color;
473        self
474    }
475
476    /// Set target FPS cap (0 = unlimited).
477    pub fn target_fps(mut self, fps: u32) -> Self {
478        self.config.target_fps = fps;
479        self
480    }
481
482    /// Set minimum window size.
483    pub fn min_size(mut self, w: u32, h: u32) -> Self {
484        self.config.min_size = Some((w, h));
485        self
486    }
487
488    /// Set the window icon from a file path.
489    pub fn icon(mut self, path: &str) -> Self {
490        self.config.icon_path = Some(path.to_string());
491        self
492    }
493
494    /// Run the game with the given game state.
495    ///
496    /// This is the main entry point. It creates the window, initializes
497    /// the context, and runs the game loop until the game exits.
498    pub fn run<S: GameState>(self, mut state: S) {
499        let mut ctx = Context::new(self.config);
500
501        state.on_enter(&mut ctx);
502
503        // Main game loop
504        // In a real implementation, this would use miniquad's event loop.
505        // For the API definition, we simulate the loop structure.
506
507        #[cfg(feature = "audio")]
508        {
509            // Initialize audio output (rodio)
510        }
511
512        loop {
513            // 1. Tick time
514            ctx.time.tick();
515
516            // 2. Process input events
517            ctx.input.keyboard.pressed_this_frame.clear();
518            ctx.input.keyboard.released_this_frame.clear();
519            ctx.input.mouse.pressed_this_frame.clear();
520            ctx.input.mouse.released_this_frame.clear();
521            ctx.input.mouse.scroll = Vec2::ZERO;
522            ctx.input.mouse.delta = Vec2::ZERO;
523
524            // 3. Fixed updates
525            let fixed_steps = ctx.time.fixed_step_count();
526            for _ in 0..fixed_steps {
527                state.fixed_update(&mut ctx);
528            }
529
530            // 4. Variable update
531            if !ctx.paused {
532                state.update(&mut ctx);
533            }
534
535            // 5. Update subsystems
536            #[cfg(feature = "audio")]
537            ctx.audio.update(ctx.time.delta() as f32);
538
539            #[cfg(feature = "particles")]
540            for emitter in &mut ctx.particles {
541                emitter.update(ctx.time.delta() as f32);
542            }
543
544            #[cfg(feature = "tween")]
545            { let _ = ctx.tweens.update(ctx.time.delta() as f32); }
546
547            #[cfg(feature = "ui")]
548            ctx.ui_state.update_animations(
549                ctx.time.delta() as f32,
550                ctx.ui_theme.animation_speed,
551            );
552
553            // Update camera
554            ctx.graphics.camera.update(ctx.time.delta() as f32);
555
556            // 6. Render
557            state.render(&mut ctx);
558
559            // 7. Flush draw commands (in real impl, submit to GPU)
560            let _commands = ctx.graphics.flush();
561
562            // 8. Check quit
563            if ctx.should_quit {
564                break;
565            }
566        }
567
568        state.on_exit(&mut ctx);
569    }
570}
571
572impl Default for Game {
573    fn default() -> Self {
574        Self::new()
575    }
576}