Skip to main content

goud_engine/sdk/
game.rs

1//! Main game abstraction for Rust-native game development.
2//!
3//! Contains [`GoudGame`], the primary entry point managing the ECS world,
4//! game loop, and convenient methods for entity and component operations.
5
6use crate::core::error::GoudResult;
7use crate::ecs::{Component, Entity, World};
8
9#[cfg(feature = "native")]
10use crate::ecs::InputManager;
11#[cfg(feature = "native")]
12use crate::libs::graphics::backend::opengl::OpenGLBackend;
13#[cfg(feature = "native")]
14use crate::libs::graphics::renderer3d::Renderer3D;
15#[cfg(feature = "native")]
16use crate::libs::graphics::sprite_batch::SpriteBatch;
17#[cfg(feature = "native")]
18use crate::libs::platform::PlatformBackend;
19
20use super::entity_builder::EntityBuilder;
21use super::game_config::{GameConfig, GameContext};
22
23/// The main game instance managing the ECS world and game loop.
24///
25/// # Example
26///
27/// ```rust
28/// use goud_engine::sdk::{GoudGame, GameConfig};
29/// use goud_engine::sdk::components::Transform2D;
30/// use goud_engine::core::math::Vec2;
31///
32/// let mut game = GoudGame::new(GameConfig::default()).unwrap();
33/// let player = game.spawn()
34///     .with(Transform2D::from_position(Vec2::new(400.0, 300.0)))
35///     .build();
36/// ```
37pub struct GoudGame {
38    /// The ECS world containing all game state.
39    pub(crate) world: World,
40
41    /// Game configuration.
42    pub(crate) config: GameConfig,
43
44    /// Runtime context for the game loop.
45    pub(crate) context: GameContext,
46
47    /// Whether the game has been initialized.
48    pub(crate) initialized: bool,
49
50    // =========================================================================
51    // Native-only fields (require windowing + OpenGL)
52    // =========================================================================
53    /// Platform backend for window management (GLFW).
54    #[cfg(feature = "native")]
55    pub(crate) platform: Option<Box<dyn PlatformBackend>>,
56
57    /// OpenGL rendering backend.
58    #[cfg(feature = "native")]
59    pub(crate) render_backend: Option<OpenGLBackend>,
60
61    /// Input manager for keyboard/mouse/gamepad state.
62    #[cfg(feature = "native")]
63    pub(crate) input_manager: InputManager,
64
65    /// 2D sprite batch renderer.
66    #[cfg(feature = "native")]
67    pub(crate) sprite_batch: Option<SpriteBatch<OpenGLBackend>>,
68
69    /// Asset server for loading and managing assets.
70    #[cfg(feature = "native")]
71    pub(crate) asset_server: Option<crate::assets::AssetServer>,
72
73    /// 3D renderer for primitives, lighting, and camera.
74    #[cfg(feature = "native")]
75    pub(crate) renderer_3d: Option<Renderer3D>,
76
77    /// GPU resources for immediate-mode sprite/quad rendering.
78    #[cfg(feature = "native")]
79    pub(crate) immediate_state: Option<super::rendering::ImmediateRenderState>,
80}
81
82impl GoudGame {
83    /// Creates a new game instance with the given configuration.
84    ///
85    /// This creates a headless game instance suitable for testing and
86    /// non-graphical use. For a windowed game with rendering, use
87    /// [`with_platform`](Self::with_platform) instead.
88    pub fn new(config: GameConfig) -> GoudResult<Self> {
89        let window_size = (config.width, config.height);
90        Ok(Self {
91            world: World::new(),
92            config,
93            context: GameContext::new(window_size),
94            initialized: false,
95            #[cfg(feature = "native")]
96            platform: None,
97            #[cfg(feature = "native")]
98            render_backend: None,
99            #[cfg(feature = "native")]
100            input_manager: InputManager::default(),
101            #[cfg(feature = "native")]
102            sprite_batch: None,
103            #[cfg(feature = "native")]
104            asset_server: None,
105            #[cfg(feature = "native")]
106            renderer_3d: None,
107            #[cfg(feature = "native")]
108            immediate_state: None,
109        })
110    }
111
112    /// Creates a game with default configuration.
113    pub fn default_game() -> GoudResult<Self> {
114        Self::new(GameConfig::default())
115    }
116
117    /// Creates a windowed game instance with a GLFW platform backend.
118    ///
119    /// This initializes a GLFW window with an OpenGL 3.3 Core context,
120    /// sets up the sprite batch renderer, and prepares the asset server.
121    ///
122    /// # Errors
123    ///
124    /// Returns an error if GLFW initialization or window creation fails.
125    #[cfg(feature = "native")]
126    pub fn with_platform(config: GameConfig) -> GoudResult<Self> {
127        use crate::libs::platform::glfw_platform::GlfwPlatform;
128        use crate::libs::platform::WindowConfig;
129
130        let window_config = WindowConfig {
131            width: config.width,
132            height: config.height,
133            title: config.title.clone(),
134            vsync: config.vsync,
135            resizable: config.resizable,
136        };
137
138        let platform = GlfwPlatform::new(&window_config)?;
139        let window_size = (config.width, config.height);
140
141        Ok(Self {
142            world: World::new(),
143            config,
144            context: GameContext::new(window_size),
145            initialized: false,
146            platform: Some(Box::new(platform)),
147            render_backend: None,
148            input_manager: InputManager::default(),
149            sprite_batch: None,
150            asset_server: None,
151            renderer_3d: None,
152            immediate_state: None,
153        })
154    }
155
156    /// Returns a reference to the ECS world.
157    #[inline]
158    pub fn world(&self) -> &World {
159        &self.world
160    }
161
162    /// Returns a mutable reference to the ECS world.
163    #[inline]
164    pub fn world_mut(&mut self) -> &mut World {
165        &mut self.world
166    }
167
168    /// Creates an entity builder for fluent entity creation.
169    #[inline]
170    pub fn spawn(&mut self) -> EntityBuilder<'_> {
171        EntityBuilder::new(&mut self.world)
172    }
173
174    /// Spawns an empty entity with no components.
175    #[inline]
176    pub fn spawn_empty(&mut self) -> Entity {
177        self.world.spawn_empty()
178    }
179
180    /// Spawns multiple empty entities at once.
181    #[inline]
182    pub fn spawn_batch(&mut self, count: usize) -> Vec<Entity> {
183        self.world.spawn_batch(count)
184    }
185
186    /// Despawns an entity and removes all its components.
187    #[inline]
188    pub fn despawn(&mut self, entity: Entity) -> bool {
189        self.world.despawn(entity)
190    }
191
192    /// Gets a reference to a component on an entity.
193    #[inline]
194    pub fn get<T: Component>(&self, entity: Entity) -> Option<&T> {
195        self.world.get::<T>(entity)
196    }
197
198    /// Gets a mutable reference to a component on an entity.
199    #[inline]
200    pub fn get_mut<T: Component>(&mut self, entity: Entity) -> Option<&mut T> {
201        self.world.get_mut::<T>(entity)
202    }
203
204    /// Adds or replaces a component on an entity.
205    #[inline]
206    pub fn insert<T: Component>(&mut self, entity: Entity, component: T) {
207        self.world.insert(entity, component);
208    }
209
210    /// Removes a component from an entity.
211    #[inline]
212    pub fn remove<T: Component>(&mut self, entity: Entity) -> Option<T> {
213        self.world.remove::<T>(entity)
214    }
215
216    /// Checks if an entity has a specific component.
217    #[inline]
218    pub fn has<T: Component>(&self, entity: Entity) -> bool {
219        self.world.has::<T>(entity)
220    }
221
222    /// Returns the number of entities in the world.
223    #[inline]
224    pub fn entity_count(&self) -> usize {
225        self.world.entity_count()
226    }
227
228    /// Checks if an entity is alive.
229    #[inline]
230    pub fn is_alive(&self, entity: Entity) -> bool {
231        self.world.is_alive(entity)
232    }
233
234    /// Returns the game configuration.
235    #[inline]
236    pub fn config(&self) -> &GameConfig {
237        &self.config
238    }
239
240    /// Returns the window title.
241    #[inline]
242    pub fn title(&self) -> &str {
243        &self.config.title
244    }
245
246    /// Returns the window dimensions.
247    #[inline]
248    pub fn window_size(&self) -> (u32, u32) {
249        (self.config.width, self.config.height)
250    }
251
252    /// Runs the game loop with the given update callback.
253    pub fn run<F>(&mut self, mut update: F)
254    where
255        F: FnMut(&mut GameContext, &mut World),
256    {
257        self.initialized = true;
258
259        // Simple game loop (actual implementation would use GLFW/window events)
260        let frame_time = if self.config.target_fps > 0 {
261            1.0 / self.config.target_fps as f32
262        } else {
263            1.0 / 60.0 // Default to 60 FPS for simulation
264        };
265
266        // For now, just run a few frames to demonstrate the API
267        // Real implementation would integrate with windowing system
268        while self.context.is_running() {
269            self.context.update(frame_time);
270            update(&mut self.context, &mut self.world);
271
272            // Safety: Limit iterations in tests/examples without actual window
273            if self.context.frame_count() > 10000 {
274                break;
275            }
276        }
277    }
278
279    /// Runs a single frame update.
280    pub fn update_frame<F>(&mut self, delta_time: f32, mut update: F)
281    where
282        F: FnMut(&mut GameContext, &mut World),
283    {
284        self.context.update(delta_time);
285        update(&mut self.context, &mut self.world);
286    }
287
288    /// Returns the current frame count.
289    #[inline]
290    pub fn frame_count(&self) -> u64 {
291        self.context.frame_count()
292    }
293
294    /// Returns the total time elapsed since game start.
295    #[inline]
296    pub fn total_time(&self) -> f32 {
297        self.context.total_time()
298    }
299
300    /// Returns the current FPS.
301    #[inline]
302    pub fn fps(&self) -> f32 {
303        self.context.fps()
304    }
305
306    /// Returns true if the game has been initialized.
307    #[inline]
308    pub fn is_initialized(&self) -> bool {
309        self.initialized
310    }
311}
312
313impl Default for GoudGame {
314    fn default() -> Self {
315        Self::new(GameConfig::default()).expect("Failed to create default GoudGame")
316    }
317}
318
319impl std::fmt::Debug for GoudGame {
320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        f.debug_struct("GoudGame")
322            .field("config", &self.config)
323            .field("entity_count", &self.world.entity_count())
324            .field("initialized", &self.initialized)
325            .finish()
326    }
327}
328
329// =============================================================================
330// Tests
331// =============================================================================
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336    use crate::core::math::Vec2;
337    use crate::sdk::components::{GlobalTransform2D, Transform2D};
338
339    #[test]
340    fn test_goud_game_new() {
341        let game = GoudGame::new(GameConfig::default()).unwrap();
342        assert_eq!(game.entity_count(), 0);
343        assert!(!game.is_initialized());
344    }
345
346    #[test]
347    fn test_goud_game_default() {
348        let game = GoudGame::default();
349        assert_eq!(game.title(), "GoudEngine Game");
350        assert_eq!(game.window_size(), (800, 600));
351    }
352
353    #[test]
354    fn test_goud_game_spawn() {
355        let mut game = GoudGame::default();
356
357        let entity = game
358            .spawn()
359            .with(Transform2D::from_position(Vec2::new(100.0, 100.0)))
360            .build();
361
362        assert!(game.is_alive(entity));
363        assert!(game.has::<Transform2D>(entity));
364        assert_eq!(game.entity_count(), 1);
365    }
366
367    #[test]
368    fn test_goud_game_spawn_empty() {
369        let mut game = GoudGame::default();
370
371        let entity = game.spawn_empty();
372        assert!(game.is_alive(entity));
373        assert_eq!(game.entity_count(), 1);
374    }
375
376    #[test]
377    fn test_goud_game_spawn_batch() {
378        let mut game = GoudGame::default();
379
380        let entities = game.spawn_batch(100);
381        assert_eq!(entities.len(), 100);
382        assert_eq!(game.entity_count(), 100);
383
384        for entity in entities {
385            assert!(game.is_alive(entity));
386        }
387    }
388
389    #[test]
390    fn test_goud_game_despawn() {
391        let mut game = GoudGame::default();
392
393        let entity = game.spawn_empty();
394        assert!(game.is_alive(entity));
395
396        let despawned = game.despawn(entity);
397        assert!(despawned);
398        assert!(!game.is_alive(entity));
399    }
400
401    #[test]
402    fn test_goud_game_component_operations() {
403        let mut game = GoudGame::default();
404        let entity = game.spawn_empty();
405
406        // Insert
407        game.insert(entity, Transform2D::from_position(Vec2::new(5.0, 10.0)));
408        assert!(game.has::<Transform2D>(entity));
409
410        // Get
411        let transform = game.get::<Transform2D>(entity).unwrap();
412        assert_eq!(transform.position, Vec2::new(5.0, 10.0));
413
414        // Get mut
415        {
416            let transform = game.get_mut::<Transform2D>(entity).unwrap();
417            transform.position.x = 100.0;
418        }
419        let transform = game.get::<Transform2D>(entity).unwrap();
420        assert_eq!(transform.position.x, 100.0);
421
422        // Remove
423        let removed = game.remove::<Transform2D>(entity);
424        assert!(removed.is_some());
425        assert!(!game.has::<Transform2D>(entity));
426    }
427
428    #[test]
429    fn test_goud_game_world_access() {
430        let mut game = GoudGame::default();
431
432        // Immutable access
433        assert_eq!(game.world().entity_count(), 0);
434
435        // Mutable access
436        let entity = game.world_mut().spawn_empty();
437        assert!(game.world().is_alive(entity));
438    }
439
440    #[test]
441    fn test_goud_game_update_frame() {
442        let mut game = GoudGame::default();
443        let mut update_count = 0;
444
445        game.update_frame(0.016, |_ctx, _world| {
446            update_count += 1;
447        });
448
449        assert_eq!(update_count, 1);
450        assert_eq!(game.frame_count(), 1);
451    }
452
453    #[test]
454    fn test_goud_game_run_with_quit() {
455        let mut game = GoudGame::default();
456        let mut frame_count = 0;
457
458        game.run(|ctx, _world| {
459            frame_count += 1;
460            if frame_count >= 5 {
461                ctx.quit();
462            }
463        });
464
465        assert_eq!(frame_count, 5);
466        assert!(game.is_initialized());
467    }
468
469    #[test]
470    fn test_goud_game_debug() {
471        let game = GoudGame::default();
472        let debug = format!("{:?}", game);
473
474        assert!(debug.contains("GoudGame"));
475        assert!(debug.contains("config"));
476        assert!(debug.contains("entity_count"));
477    }
478
479    // =========================================================================
480    // Integration Tests
481    // =========================================================================
482
483    #[test]
484    fn test_full_game_workflow() {
485        // Create game
486        let mut game = GoudGame::new(GameConfig::new("Test", 800, 600)).unwrap();
487
488        // Spawn player with components
489        let player = game
490            .spawn()
491            .with(Transform2D::from_position(Vec2::new(400.0, 300.0)))
492            .with(GlobalTransform2D::IDENTITY)
493            .build();
494
495        // Spawn enemies
496        let mut enemies = Vec::new();
497        for i in 0..10 {
498            let enemy = game
499                .spawn()
500                .with(Transform2D::from_position(Vec2::new(
501                    i as f32 * 50.0,
502                    100.0,
503                )))
504                .build();
505            enemies.push(enemy);
506        }
507
508        assert_eq!(game.entity_count(), 11); // 1 player + 10 enemies
509
510        // Run a few frames
511        let mut player_moved = false;
512        game.run(|ctx, world| {
513            // Move player
514            if let Some(transform) = world.get_mut::<Transform2D>(player) {
515                transform.position.x += 10.0 * ctx.delta_time();
516                player_moved = true;
517            }
518
519            // Quit after a few frames
520            if ctx.frame_count() >= 3 {
521                ctx.quit();
522            }
523        });
524
525        assert!(player_moved);
526        assert!(game.is_alive(player));
527    }
528}