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}