From 88a21040cd757d755d8b5fac90d76b7c4a345975 Mon Sep 17 00:00:00 2001 From: reo Date: Mon, 21 Jul 2025 23:52:32 +0300 Subject: [PATCH] Giant refactor for a better event-driven architecture --- Cargo.lock | 10 ++ raidillon_core/Cargo.toml | 5 + raidillon_core/src/assets.rs | 95 ++++++++++++++++ raidillon_core/src/engine.rs | 80 ++++++++++++++ raidillon_core/src/events.rs | 60 ++++++++++ raidillon_core/src/lib.rs | 8 ++ raidillon_core/src/systems.rs | 45 ++++++++ raidillon_ecs/src/lib.rs | 2 +- raidillon_game/Cargo.toml | 1 + raidillon_game/src/game_state.rs | 88 +++++++++++++++ raidillon_game/src/lib.rs | 4 + raidillon_game/src/main.rs | 153 +++++++++++++++----------- raidillon_game/src/main_clean.rs | 116 +++++++++++++++++++ raidillon_input/Cargo.toml | 5 +- raidillon_input/src/camera_system.rs | 109 ++++++++++++++++++ raidillon_input/src/input_system.rs | 64 +++++++++++ raidillon_input/src/lib.rs | 5 + raidillon_render/Cargo.toml | 1 + raidillon_render/src/lib.rs | 3 + raidillon_render/src/model.rs | 6 + raidillon_render/src/render.rs | 93 +++++++++++++++- raidillon_render/src/render_system.rs | 50 +++++++++ 22 files changed, 936 insertions(+), 67 deletions(-) create mode 100644 raidillon_core/src/assets.rs create mode 100644 raidillon_core/src/engine.rs create mode 100644 raidillon_core/src/events.rs create mode 100644 raidillon_core/src/systems.rs create mode 100644 raidillon_game/src/game_state.rs create mode 100644 raidillon_game/src/lib.rs create mode 100644 raidillon_game/src/main_clean.rs create mode 100644 raidillon_input/src/camera_system.rs create mode 100644 raidillon_input/src/input_system.rs create mode 100644 raidillon_render/src/render_system.rs diff --git a/Cargo.lock b/Cargo.lock index 0a091ac..b5f6919 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1728,6 +1728,11 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "raidillon_core" version = "0.1.0" +dependencies = [ + "anyhow", + "glam", + "hecs", +] [[package]] name = "raidillon_ecs" @@ -1743,6 +1748,7 @@ version = "0.1.0" dependencies = [ "anyhow", "glam", + "glium", "hecs", "raidillon_core", "raidillon_ecs", @@ -1757,6 +1763,9 @@ name = "raidillon_input" version = "0.1.0" dependencies = [ "glam", + "hecs", + "raidillon_core", + "raidillon_render", "winit", ] @@ -1771,6 +1780,7 @@ dependencies = [ "glutin", "hecs", "image", + "raidillon_core", "raidillon_ecs", "winit", ] diff --git a/raidillon_core/Cargo.toml b/raidillon_core/Cargo.toml index 1d15e30..27b9c1e 100644 --- a/raidillon_core/Cargo.toml +++ b/raidillon_core/Cargo.toml @@ -2,3 +2,8 @@ name = "raidillon_core" version = "0.1.0" edition = "2021" + +[dependencies] +anyhow = "1.0.98" +glam = "0.30.4" +hecs = "0.10.5" diff --git a/raidillon_core/src/assets.rs b/raidillon_core/src/assets.rs new file mode 100644 index 0000000..04dd337 --- /dev/null +++ b/raidillon_core/src/assets.rs @@ -0,0 +1,95 @@ +use std::collections::HashMap; + +// Forward declarations - these will be from other crates +pub trait Model {} +pub trait Material {} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct ModelId(pub usize); + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct MaterialId(pub usize); + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct TextureHandle(pub usize); + +pub struct AssetManager { + models: Vec>, + materials: Vec>, + textures: HashMap, + model_cache: HashMap, + next_texture_id: usize, +} + +impl AssetManager { + pub fn new() -> Self { + Self { + models: Vec::new(), + materials: Vec::new(), + textures: HashMap::new(), + model_cache: HashMap::new(), + next_texture_id: 0, + } + } + + pub fn add_model(&mut self, model: Box) -> ModelId { + let id = ModelId(self.models.len()); + self.models.push(model); + id + } + + pub fn cache_model(&mut self, path: String, model: Box) -> ModelId { + if let Some(&cached_id) = self.model_cache.get(&path) { + return cached_id; + } + + let model_id = self.add_model(model); + self.model_cache.insert(path, model_id); + model_id + } + + pub fn get_model(&self, id: ModelId) -> Option<&M> { + self.models.get(id.0).map(|boxed| boxed.as_ref()) + } + + pub fn get_model_mut(&mut self, id: ModelId) -> Option<&mut M> { + self.models.get_mut(id.0).map(|boxed| boxed.as_mut()) + } + + pub fn add_material(&mut self, material: Box) -> MaterialId { + let id = MaterialId(self.materials.len()); + self.materials.push(material); + id + } + + pub fn get_material(&self, id: MaterialId) -> Option<&Mat> { + self.materials.get(id.0).map(|boxed| boxed.as_ref()) + } + + pub fn add_texture(&mut self, name: String) -> TextureHandle { + if let Some(&handle) = self.textures.get(&name) { + return handle; + } + + let handle = TextureHandle(self.next_texture_id); + self.next_texture_id += 1; + self.textures.insert(name, handle); + handle + } + + pub fn get_texture_handle(&self, name: &str) -> Option { + self.textures.get(name).copied() + } + + pub fn model_count(&self) -> usize { + self.models.len() + } + + pub fn material_count(&self) -> usize { + self.materials.len() + } + + pub fn clear_cache(&mut self) { + self.model_cache.clear(); + } +} \ No newline at end of file diff --git a/raidillon_core/src/engine.rs b/raidillon_core/src/engine.rs new file mode 100644 index 0000000..13b82ba --- /dev/null +++ b/raidillon_core/src/engine.rs @@ -0,0 +1,80 @@ +use hecs::World; +use crate::{ + Time, EventBus, GameEvent, SystemRegistry, + AssetManager, Model, Material, ModelId +}; + +pub struct Engine { + pub world: World, + pub systems: SystemRegistry, + pub assets: AssetManager, + pub events: EventBus, + pub time: Time, +} + +impl Engine { + pub fn new() -> Self { + let systems = SystemRegistry::new(); + + Self { + world: World::new(), + systems, + assets: AssetManager::new(), + events: EventBus::new(), + time: Time::new(), + } + } + + pub fn add_system(&mut self, system: S) { + self.systems.add_system(system); + } + + pub fn update(&mut self) { + self.time.tick(); + let dt = self.time.delta_seconds(); + + // Update all systems + self.systems.update_all(&mut self.world, &self.assets, &mut self.events, dt); + + // Process events + self.events.process(); + } + + pub fn handle_window_event(&mut self, event: &GameEvent) { + self.events.emit(event.clone()); + self.systems.handle_event_for_all(event, &mut self.world); + } + + pub fn load_model(&mut self, path: &str) -> anyhow::Result { + // This is a placeholder - in a real implementation, we'd need to + // coordinate with the render system to actually load the model + // For now, just return a dummy ID + Ok(ModelId(0)) + } + + pub fn spawn_entity_with_model(&mut self, model_id: ModelId) -> hecs::Entity { + // This would need proper Transform and ModelHandle types + // For now, return a placeholder entity + self.world.spawn(()) + } + + pub fn delta_time(&self) -> f32 { + self.time.delta_seconds() + } + + pub fn emit_event(&mut self, event: GameEvent) { + self.events.emit(event); + } + + pub fn world(&self) -> &World { + &self.world + } + + pub fn world_mut(&mut self) -> &mut World { + &mut self.world + } + + pub fn system_count(&self) -> usize { + self.systems.system_count() + } +} \ No newline at end of file diff --git a/raidillon_core/src/events.rs b/raidillon_core/src/events.rs new file mode 100644 index 0000000..3a7dc1e --- /dev/null +++ b/raidillon_core/src/events.rs @@ -0,0 +1,60 @@ +use glam::Vec3; +use hecs::Entity; + +#[derive(Debug, Clone)] +pub enum GameEvent { + InputAction(InputAction), + CameraMove { position: Vec3, front: Vec3 }, + WindowResize { width: u32, height: u32 }, + EntitySpawned(Entity), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum InputAction { + MoveForward, + MoveBackward, + MoveLeft, + MoveRight, +} + +pub trait EventHandler { + fn handle(&mut self, event: &GameEvent); +} + +pub struct EventBus { + events: Vec, + handlers: Vec>, +} + +impl EventBus { + pub fn new() -> Self { + Self { + events: Vec::new(), + handlers: Vec::new(), + } + } + + pub fn emit(&mut self, event: GameEvent) { + self.events.push(event); + } + + pub fn subscribe(&mut self, handler: H) { + self.handlers.push(Box::new(handler)); + } + + pub fn process(&mut self) { + for event in self.events.drain(..) { + for handler in &mut self.handlers { + handler.handle(&event); + } + } + } + + pub fn has_events(&self) -> bool { + !self.events.is_empty() + } + + pub fn events(&self) -> &[GameEvent] { + &self.events + } +} \ No newline at end of file diff --git a/raidillon_core/src/lib.rs b/raidillon_core/src/lib.rs index 230f8bb..d646dcb 100644 --- a/raidillon_core/src/lib.rs +++ b/raidillon_core/src/lib.rs @@ -1,3 +1,11 @@ pub mod time; +pub mod events; +pub mod assets; +pub mod systems; +pub mod engine; pub use time::Time; +pub use events::{GameEvent, InputAction, EventHandler, EventBus}; +pub use assets::{AssetManager, ModelId, MaterialId, TextureHandle, Model, Material}; +pub use systems::{System, SystemRegistry}; +pub use engine::Engine; diff --git a/raidillon_core/src/systems.rs b/raidillon_core/src/systems.rs new file mode 100644 index 0000000..3ccd41e --- /dev/null +++ b/raidillon_core/src/systems.rs @@ -0,0 +1,45 @@ +use hecs::World; +use crate::assets::{AssetManager, Model, Material}; +use crate::events::{EventBus, GameEvent}; + +pub trait System { + fn update(&mut self, world: &mut World, resources: &AssetManager, events: &mut EventBus, dt: f32); + fn handle_event(&mut self, event: &GameEvent, world: &mut World); + fn name(&self) -> &'static str; +} + +pub struct SystemRegistry { + systems: Vec>, +} + +impl SystemRegistry { + pub fn new() -> Self { + Self { + systems: Vec::new(), + } + } + + pub fn add_system(&mut self, system: S) { + self.systems.push(Box::new(system)); + } + + pub fn update_all(&mut self, world: &mut World, resources: &AssetManager, events: &mut EventBus, dt: f32) { + for system in &mut self.systems { + system.update(world, resources, events, dt); + } + } + + pub fn handle_event_for_all(&mut self, event: &GameEvent, world: &mut World) { + for system in &mut self.systems { + system.handle_event(event, world); + } + } + + pub fn system_count(&self) -> usize { + self.systems.len() + } + + pub fn clear(&mut self) { + self.systems.clear(); + } +} \ No newline at end of file diff --git a/raidillon_ecs/src/lib.rs b/raidillon_ecs/src/lib.rs index 19a3e69..0a15576 100644 --- a/raidillon_ecs/src/lib.rs +++ b/raidillon_ecs/src/lib.rs @@ -13,5 +13,5 @@ impl Transform { } } -#[derive(Clone)] +#[derive(Copy, Clone, Debug)] pub struct ModelHandle(pub usize); diff --git a/raidillon_game/Cargo.toml b/raidillon_game/Cargo.toml index c127172..08fe8cb 100644 --- a/raidillon_game/Cargo.toml +++ b/raidillon_game/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" anyhow = "1.0.98" glam = "0.30.4" winit = "0.30" +glium = { version = "0.35.0", features = ["glutin_backend", "simple_window_builder"] } raidillon_render = { path = "../raidillon_render" } raidillon_ecs = { path = "../raidillon_ecs" } raidillon_ui = { path = "../raidillon_ui" } diff --git a/raidillon_game/src/game_state.rs b/raidillon_game/src/game_state.rs new file mode 100644 index 0000000..c57117b --- /dev/null +++ b/raidillon_game/src/game_state.rs @@ -0,0 +1,88 @@ +use anyhow; +use glam::{Quat, Vec3}; +use hecs::{Entity, World}; +use raidillon_ecs::{Transform, ModelHandle}; +use raidillon_render::{Camera, ModelId}; +use raidillon_core::InputAction; + +pub struct GameState { + pub world: World, + pub camera_entity: Entity, + pub object_entity: Entity, + pub ground_entity: Entity, +} + +impl GameState { + pub fn new() -> Self { + let mut world = World::new(); + + // Create camera entity + let camera_entity = world.spawn((Camera { + eye: Vec3::new(0.0, 0.0, 2.0), + center: Vec3::ZERO, + up: Vec3::Y, + fovy: 60_f32.to_radians(), + aspect: 1280.0 / 720.0, // default aspect ratio + znear: 0.1, + zfar: 100.0, + },)); + + // Create placeholder entities for object and ground (will be properly loaded later) + let object_entity = world.spawn(( + Transform { + translation: Vec3::new(0.0, -2.5, -5.0), + rotation: Quat::IDENTITY, + scale: Vec3::new(0.01, 0.01, 0.01), + }, + ModelHandle(0), + )); + + let ground_entity = world.spawn(( + Transform { + translation: Vec3::new(0.0, -1.5, 0.0), + rotation: Quat::IDENTITY, + scale: Vec3::new(1.0, 1.0, 1.0), + }, + ModelHandle(1), + )); + + Self { + world, + camera_entity, + object_entity, + ground_entity, + } + } + + pub fn update(&mut self, _dt: f32) { + // Game state update logic will go here + // Camera updates are now handled by CameraSystem + } + + pub fn resize_camera(&mut self, width: u32, height: u32) { + if let Ok(cam) = self.world.query_one_mut::<&mut Camera>(self.camera_entity) { + cam.aspect = width as f32 / height as f32; + } + } + + pub fn world(&self) -> &World { + &self.world + } + + pub fn world_mut(&mut self) -> &mut World { + &mut self.world + } + + pub fn spawn_model(&mut self, model_id: ModelId, transform: Transform) -> Entity { + self.world.spawn((transform, ModelHandle(model_id.0))) + } + + pub fn update_entity_model(&mut self, entity: Entity, model_id: ModelId) -> anyhow::Result<()> { + if let Ok(model_handle) = self.world.query_one_mut::<&mut ModelHandle>(entity) { + model_handle.0 = model_id.0; + Ok(()) + } else { + Err(anyhow::anyhow!("Entity does not have a ModelHandle component")) + } + } +} \ No newline at end of file diff --git a/raidillon_game/src/lib.rs b/raidillon_game/src/lib.rs new file mode 100644 index 0000000..e40c650 --- /dev/null +++ b/raidillon_game/src/lib.rs @@ -0,0 +1,4 @@ +pub mod game_state; + +pub use game_state::GameState; +pub use raidillon_core::InputAction; \ No newline at end of file diff --git a/raidillon_game/src/main.rs b/raidillon_game/src/main.rs index 2373c5d..9843297 100644 --- a/raidillon_game/src/main.rs +++ b/raidillon_game/src/main.rs @@ -1,20 +1,52 @@ use anyhow::Result; use glam::{Quat, Vec3, EulerRot}; -use raidillon_core::Time; +use raidillon_core::{Time, EventBus, GameEvent, InputAction, System, SystemRegistry, AssetManager, Model, Material}; use raidillon_ecs::Transform; -use raidillon_render::{Camera, ECSRenderer, init_render_window, DisplayHandle}; +use raidillon_render::{RenderSystem, init_render_window, DisplayHandle}; use raidillon_ui::Gui; -use raidillon_input::{Input, FPSCameraController}; -use winit::keyboard::KeyCode; +use raidillon_input::{InputSystem, CameraSystem}; +use raidillon_game::GameState; use winit::window::CursorGrabMode; use winit::event::MouseButton; +use hecs::World; -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -enum Action { - MoveForward, - MoveBackward, - MoveLeft, - MoveRight, +// Wrapper to make RenderSystem implement the System trait +struct RenderSystemWrapper { + render_system: RenderSystem, +} + +impl RenderSystemWrapper { + fn new(display: DisplayHandle) -> anyhow::Result { + Ok(Self { + render_system: RenderSystem::new(display)?, + }) + } + + fn load_model(&mut self, path: &str) -> anyhow::Result { + self.render_system.load_model(path) + } + + fn render(&mut self, world: &World, target: &mut impl glium::Surface) { + self.render_system.render(world, target) + } + + fn display(&self) -> &glium::Display { + self.render_system.display() + } +} + +impl System for RenderSystemWrapper { + fn update(&mut self, _world: &mut World, _resources: &AssetManager, _events: &mut EventBus, _dt: f32) { + // Rendering is handled separately in the main loop + } + + fn handle_event(&mut self, _event: &GameEvent, _world: &mut World) { + // RenderSystem doesn't need to respond to events currently + } + + fn name(&self) -> &'static str { + "RenderSystem" + } } fn main() -> Result<()> { @@ -24,65 +56,50 @@ fn main() -> Result<()> { let (window, _display): (winit::window::Window, DisplayHandle) = init_render_window(&event_loop, "raidillon", (1280, 720))?; - // Create ECS renderer which internally owns both the world and the renderer - let mut ecsr = ECSRenderer::from_display_handle(&_display)?; + // Create game state and systems + let mut game_state = GameState::new(); + let mut render_system_wrapper = RenderSystemWrapper::new(_display.clone())?; // Dear ImGui integration let mut gui = Gui::new(&_display, &window)?; - let mut input = Input::::new(); - input.map_key(KeyCode::KeyW, Action::MoveForward); - input.map_key(KeyCode::KeyS, Action::MoveBackward); - input.map_key(KeyCode::KeyA, Action::MoveLeft); - input.map_key(KeyCode::KeyD, Action::MoveRight); + // Create system registry and register systems + let mut system_registry = SystemRegistry::new(); + let mut event_bus = EventBus::new(); + let mut input_system = InputSystem::new(); // Keep this for direct access + let mut camera_system = CameraSystem::new(game_state.camera_entity); // Keep this for direct access - let mut camera_controller = FPSCameraController::new(Vec3::new(0.0, 0.0, 2.0)); + // Register systems later when we have proper asset manager integration + // For now, manage systems directly in main loop let mut right_mouse_held = false; - let mut time = Time::new(); - let object_ent = ecsr.load_mesh_from_gltf("resources/models/tree.gltf", Transform { - translation: Vec3::new(0.0, -2.5, -5.0), - rotation: Quat::IDENTITY, - scale: Vec3::new(0.01, 0.01, 0.01), - })?; + // Load models using the RenderSystem + let object_model_id = render_system_wrapper.load_model("resources/models/tree.gltf")?; + let ground_model_id = render_system_wrapper.load_model("resources/models/plane.gltf")?; - let ground_ent = ecsr.load_mesh_from_gltf("resources/models/plane.gltf", Transform { - translation: Vec3::new(0.0, -1.5, 0.0), - rotation: Quat::IDENTITY, - scale: Vec3::new(1.0, 1.0, 1.0), - })?; + // Update the model handles in game state using the new method + game_state.update_entity_model(game_state.object_entity, object_model_id)?; + game_state.update_entity_model(game_state.ground_entity, ground_model_id)?; - - let camera_ent = { - let (w, h): (u32, u32) = window.inner_size().into(); - ecsr.world.spawn((Camera { - eye: Vec3::new(0.0, 0.0, 2.0), - center: Vec3::ZERO, - up: Vec3::Y, - fovy: 60_f32.to_radians(), - aspect: w as f32 / h as f32, - znear: 0.1, - zfar: 100.0, - },)) - }; + // Set initial camera aspect ratio + let (w, h): (u32, u32) = window.inner_size().into(); + game_state.resize_camera(w, h); event_loop .run(move |event, el| { use winit::event::{Event, WindowEvent}; gui.handle_event(&window, &event); - - input.handle_event(&event); + input_system.handle_event(&event); match event { Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => el.exit(), WindowEvent::Resized(sz) => { - ecsr.world.query_one_mut::<&mut Camera>(camera_ent).map(|mut cam| { - cam.aspect = sz.width as f32 / sz.height as f32; - }); + camera_system.resize_camera(game_state.world_mut(), sz.width, sz.height); + event_bus.emit(GameEvent::WindowResize { width: sz.width, height: sz.height }); } WindowEvent::MouseInput { state, button, .. } => { if button == MouseButton::Right { @@ -107,12 +124,13 @@ fn main() -> Result<()> { } WindowEvent::RedrawRequested => { // First render the 3D world - let mut target = ecsr.renderer.display().draw(); - ecsr.render_into(&mut target); + let mut target = render_system_wrapper.display().draw(); + render_system_wrapper.render(game_state.world(), &mut target); // Then overlay ImGui on top gui.render_with(&mut target, &window, |ui| { - if let Ok(mut tr) = ecsr.world.query_one_mut::<&mut Transform>(object_ent) { + let object_entity = game_state.object_entity; + if let Ok(tr) = game_state.world_mut().query_one_mut::<&mut Transform>(object_entity) { ui.text("Hold right click to control the camera"); ui.text("WASD to move"); @@ -146,24 +164,31 @@ fn main() -> Result<()> { }, Event::AboutToWait => { time.tick(); - - { - let dt = time.delta_seconds(); - camera_controller.update( - &input, - dt, - right_mouse_held, - (Action::MoveForward, Action::MoveBackward, Action::MoveLeft, Action::MoveRight), - ); - - if let Ok(mut cam) = ecsr.world.query_one_mut::<&mut Camera>(camera_ent) { - cam.eye = camera_controller.position; - cam.center = camera_controller.position + camera_controller.front(); + let dt = time.delta_seconds(); + + // Update input system and generate events + input_system.update(&mut event_bus, right_mouse_held); + + // Process input events for camera movement + let mouse_delta = input_system.mouse_delta(); + + // Handle camera input actions + for event in event_bus.events() { + if let GameEvent::InputAction(action) = event { + camera_system.handle_input_action(*action, dt); } } + + // Update camera with mouse movement + camera_system.update(game_state.world_mut(), dt, mouse_delta); + + // Update game state + game_state.update(dt); - input.end_frame(); + // Process all events + event_bus.process(); + input_system.end_frame(); gui.prepare_frame(&window); window.request_redraw(); } diff --git a/raidillon_game/src/main_clean.rs b/raidillon_game/src/main_clean.rs new file mode 100644 index 0000000..c3a8c34 --- /dev/null +++ b/raidillon_game/src/main_clean.rs @@ -0,0 +1,116 @@ +use anyhow::Result; +use raidillon_core::{Engine, GameEvent}; +use raidillon_render::{init_render_window, RenderSystem}; +use raidillon_ui::Gui; +use raidillon_input::{InputSystem, CameraSystem}; +use winit::event::{Event, WindowEvent}; +use winit::window::CursorGrabMode; +use winit::event::MouseButton; + +fn main() -> Result<()> { + let event_loop = winit::event_loop::EventLoop::builder() + .build() + .expect("create event-loop"); + + let (window, _display) = init_render_window(&event_loop, "raidillon", (1280, 720))?; + + // Create the unified engine + let mut engine = Engine::new(); + + // Create render system separately (for now, until full integration) + let mut render_system = RenderSystem::new(_display.clone())?; + + // Create GUI system + let mut gui = Gui::new(&_display, &window)?; + + // Add systems to the engine + let input_system = InputSystem::new(); + let camera_system = CameraSystem::new(engine.world().spawn(())); // placeholder camera entity + + engine.add_system(input_system); + engine.add_system(camera_system); + + // Load initial scene content + load_default_scene(&mut engine, &mut render_system)?; + + let mut right_mouse_held = false; + + event_loop + .run(move |event, el| { + gui.handle_event(&window, &event); + + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => el.exit(), + WindowEvent::Resized(sz) => { + engine.handle_window_event(&GameEvent::WindowResize { + width: sz.width, + height: sz.height + }); + } + WindowEvent::MouseInput { state, button, .. } => { + if button == MouseButton::Right { + match state { + winit::event::ElementState::Pressed => { + if window + .set_cursor_grab(CursorGrabMode::Confined) + .or_else(|_| window.set_cursor_grab(CursorGrabMode::Locked)) + .is_ok() + { + window.set_cursor_visible(false); + right_mouse_held = true; + } + } + winit::event::ElementState::Released => { + let _ = window.set_cursor_grab(CursorGrabMode::None); + window.set_cursor_visible(true); + right_mouse_held = false; + } + } + } + } + WindowEvent::RedrawRequested => { + // Update engine + engine.update(); + + // Render + let mut target = render_system.display().draw(); + render_system.render(engine.world(), &mut target); + + // Render debug UI + gui.render_with(&mut target, &window, |ui| { + render_debug_ui(&engine, ui); + }); + + target.finish().expect("Failed to swap buffers"); + } + _ => {} + }, + Event::AboutToWait => { + gui.prepare_frame(&window); + window.request_redraw(); + } + _ => {} + } + }) + .map_err(Into::into) +} + +fn load_default_scene(engine: &mut Engine, render_system: &mut RenderSystem) -> Result<()> { + // Load and setup default scene + let _tree_model = render_system.load_model("resources/models/tree.gltf")?; + let _ground_model = render_system.load_model("resources/models/plane.gltf")?; + + // Note: Full integration would require coordinating between engine and render system + // For now, this demonstrates the clean architecture structure + + println!("Loaded default scene with {} systems", engine.system_count()); + Ok(()) +} + +fn render_debug_ui(engine: &Engine, ui: &imgui::Ui) { + ui.text(format!("Engine Systems: {}", engine.system_count())); + ui.text(format!("Delta Time: {:.3}ms", engine.delta_time() * 1000.0)); + ui.text("Clean Architecture Demo"); + ui.text("This shows the unified Engine approach"); +} \ No newline at end of file diff --git a/raidillon_input/Cargo.toml b/raidillon_input/Cargo.toml index 1f1700d..abcbe66 100644 --- a/raidillon_input/Cargo.toml +++ b/raidillon_input/Cargo.toml @@ -5,4 +5,7 @@ edition = "2021" [dependencies] winit = "0.30" -glam = "0.30.4" \ No newline at end of file +glam = "0.30.4" +hecs = "0.10.5" +raidillon_core = { path = "../raidillon_core" } +raidillon_render = { path = "../raidillon_render" } \ No newline at end of file diff --git a/raidillon_input/src/camera_system.rs b/raidillon_input/src/camera_system.rs new file mode 100644 index 0000000..7925d47 --- /dev/null +++ b/raidillon_input/src/camera_system.rs @@ -0,0 +1,109 @@ +use hecs::{Entity, World}; +use raidillon_core::{EventHandler, GameEvent, InputAction, System, AssetManager, Model, Material, EventBus}; +use raidillon_render::Camera; +use crate::FPSCameraController; +use glam::Vec3; + +pub struct CameraSystem { + controller: FPSCameraController, + camera_entity: Entity, + yaw: f32, + pitch: f32, +} + +impl CameraSystem { + pub fn new(camera_entity: Entity) -> Self { + Self { + controller: FPSCameraController::new(Vec3::new(0.0, 0.0, 2.0)), + camera_entity, + yaw: -90.0, + pitch: 0.0, + } + } + + pub fn update(&mut self, world: &mut World, dt: f32, mouse_delta: (f64, f64)) { + // Apply mouse movement if there's any + if mouse_delta.0 != 0.0 || mouse_delta.1 != 0.0 { + self.yaw += mouse_delta.0 as f32 * self.controller.sensitivity; + self.pitch -= mouse_delta.1 as f32 * self.controller.sensitivity; + self.pitch = self.pitch.clamp(-89.0, 89.0); + + // Update front vector based on new yaw/pitch + let yaw_rad = self.yaw.to_radians(); + let pitch_rad = self.pitch.to_radians(); + let front = Vec3::new( + yaw_rad.cos() * pitch_rad.cos(), + pitch_rad.sin(), + yaw_rad.sin() * pitch_rad.cos(), + ).normalize(); + } + + // Update camera component in the world + if let Ok(cam) = world.query_one_mut::<&mut Camera>(self.camera_entity) { + cam.eye = self.controller.position; + cam.center = self.controller.position + self.controller.front(); + } + } + + pub fn handle_input_action(&mut self, action: InputAction, dt: f32) { + let front = self.controller.front(); + let right = front.cross(Vec3::Y).normalize(); + let frame_speed = self.controller.speed * dt; + + match action { + InputAction::MoveForward => { + self.controller.position += front * frame_speed; + } + InputAction::MoveBackward => { + self.controller.position -= front * frame_speed; + } + InputAction::MoveLeft => { + self.controller.position -= right * frame_speed; + } + InputAction::MoveRight => { + self.controller.position += right * frame_speed; + } + } + } + + pub fn resize_camera(&mut self, world: &mut World, width: u32, height: u32) { + if let Ok(cam) = world.query_one_mut::<&mut Camera>(self.camera_entity) { + cam.aspect = width as f32 / height as f32; + } + } +} + +impl EventHandler for CameraSystem { + fn handle(&mut self, event: &GameEvent) { + match event { + GameEvent::InputAction(_action) => { + // Movement will be handled separately with delta time + // This is just for event registration + } + GameEvent::WindowResize { width: _, height: _ } => { + // Window resize will be handled separately with world access + } + _ => {} + } + } +} + +impl System for CameraSystem { + fn update(&mut self, world: &mut World, _resources: &AssetManager, _events: &mut EventBus, _dt: f32) { + // Camera update logic is handled separately with mouse input + // This system mainly responds to events + } + + fn handle_event(&mut self, event: &GameEvent, world: &mut World) { + match event { + GameEvent::WindowResize { width, height } => { + self.resize_camera(world, *width, *height); + } + _ => {} + } + } + + fn name(&self) -> &'static str { + "CameraSystem" + } +} \ No newline at end of file diff --git a/raidillon_input/src/input_system.rs b/raidillon_input/src/input_system.rs new file mode 100644 index 0000000..4283b7e --- /dev/null +++ b/raidillon_input/src/input_system.rs @@ -0,0 +1,64 @@ +use raidillon_core::{EventBus, GameEvent, InputAction, System, AssetManager, Model, Material}; +use crate::Input; +use hecs::World; + +pub struct InputSystem { + input: Input, +} + +impl InputSystem { + pub fn new() -> Self { + let mut input = Input::::new(); + input.map_key(winit::keyboard::KeyCode::KeyW, InputAction::MoveForward); + input.map_key(winit::keyboard::KeyCode::KeyS, InputAction::MoveBackward); + input.map_key(winit::keyboard::KeyCode::KeyA, InputAction::MoveLeft); + input.map_key(winit::keyboard::KeyCode::KeyD, InputAction::MoveRight); + + Self { input } + } + + pub fn handle_event(&mut self, event: &winit::event::Event) { + self.input.handle_event(event); + } + + pub fn update(&mut self, event_bus: &mut EventBus, right_mouse_held: bool) { + if right_mouse_held { + if self.input.action_held(InputAction::MoveForward) { + event_bus.emit(GameEvent::InputAction(InputAction::MoveForward)); + } + if self.input.action_held(InputAction::MoveBackward) { + event_bus.emit(GameEvent::InputAction(InputAction::MoveBackward)); + } + if self.input.action_held(InputAction::MoveLeft) { + event_bus.emit(GameEvent::InputAction(InputAction::MoveLeft)); + } + if self.input.action_held(InputAction::MoveRight) { + event_bus.emit(GameEvent::InputAction(InputAction::MoveRight)); + } + } + } + + pub fn end_frame(&mut self) { + self.input.end_frame(); + } + + pub fn mouse_delta(&self) -> (f64, f64) { + self.input.mouse_delta() + } +} + +impl System for InputSystem { + fn update(&mut self, _world: &mut World, _resources: &AssetManager, _events: &mut EventBus, _dt: f32) { + // Input processing is handled separately in the main loop + // This system mainly generates events based on input state + } + + fn handle_event(&mut self, _event: &GameEvent, _world: &mut World) { + // InputSystem doesn't need to respond to events + // It generates events based on input state + } + + fn name(&self) -> &'static str { + "InputSystem" + } +} \ No newline at end of file diff --git a/raidillon_input/src/lib.rs b/raidillon_input/src/lib.rs index 25f5a95..30908cb 100644 --- a/raidillon_input/src/lib.rs +++ b/raidillon_input/src/lib.rs @@ -5,7 +5,12 @@ use winit::event::{DeviceEvent, ElementState, Event, WindowEvent}; use winit::keyboard::{KeyCode, PhysicalKey}; pub mod camera; +pub mod input_system; +pub mod camera_system; + pub use camera::FPSCameraController; +pub use input_system::InputSystem; +pub use camera_system::CameraSystem; pub struct Input { pressed_keys: HashSet, diff --git a/raidillon_render/Cargo.toml b/raidillon_render/Cargo.toml index ae6c94a..18a7279 100644 --- a/raidillon_render/Cargo.toml +++ b/raidillon_render/Cargo.toml @@ -11,5 +11,6 @@ gltf = { version = "1.4.1", features = ["import", "utils", "KHR_texture_transf glutin = { version = "0.32.3", default-features = false } hecs = "0.10.5" image = "0.25.6" +raidillon_core = { path = "../raidillon_core" } raidillon_ecs = { path = "../raidillon_ecs" } winit = "0.30" diff --git a/raidillon_render/src/lib.rs b/raidillon_render/src/lib.rs index f883c91..8a5b192 100644 --- a/raidillon_render/src/lib.rs +++ b/raidillon_render/src/lib.rs @@ -3,9 +3,12 @@ pub mod model; pub mod gltf_loader; pub mod render; pub mod ecs_renderer; +pub mod render_system; pub mod window; pub use camera::Camera; pub use render::GliumRenderer; pub use ecs_renderer::ECSRenderer; +pub use render_system::RenderSystem; +pub use raidillon_core::ModelId; pub use window::{DisplayHandle, init_window as init_render_window}; diff --git a/raidillon_render/src/model.rs b/raidillon_render/src/model.rs index e474fe4..f980b8b 100644 --- a/raidillon_render/src/model.rs +++ b/raidillon_render/src/model.rs @@ -51,7 +51,13 @@ impl Default for Material { } } +// Implement the Material trait from raidillon_core +impl raidillon_core::Material for Material {} + pub struct Model { pub mesh: Mesh, pub material: Material, } + +// Implement the Model trait from raidillon_core +impl raidillon_core::Model for Model {} diff --git a/raidillon_render/src/render.rs b/raidillon_render/src/render.rs index 1d7554d..df6be7a 100644 --- a/raidillon_render/src/render.rs +++ b/raidillon_render/src/render.rs @@ -1,6 +1,7 @@ use crate::camera::Camera; use raidillon_ecs::{ModelHandle, Transform}; -use crate::model::{Model, Mesh}; +use crate::model::{Model, Material, Mesh}; +use raidillon_core::AssetManager; use glium::texture::{RawImage2d, SrgbTexture2d}; use glium::{uniform, Program, Surface}; use glium::uniforms::{MinifySamplerFilter, MagnifySamplerFilter, SamplerWrapFunction}; @@ -164,4 +165,94 @@ impl GliumRenderer { pub fn display(&self) -> &glium::Display { &self.display } + + pub fn render_into_with_assets(&mut self, world: &World, assets: &AssetManager, target: &mut S) { + target.clear_color_and_depth((0.1, 0.1, 0.15, 1.0), 1.0); + self.draw_scene_with_assets(world, assets, target); + } + + fn draw_scene_with_assets(&mut self, world: &World, assets: &AssetManager, target: &mut S) { + let cam = match world.query::<&Camera>().iter().next() { + Some((_, cam)) => *cam, + None => { + eprintln!("[renderer] No camera component found. Skipping frame"); + return; + } + }; + + // Direction from the light source (0,+Y) towards the scene. + let light_dir: Vec3 = Vec3::new(0.0, -1.0, 0.0).normalize(); + + for (_, (tr, mh)) in world.query::<(&Transform, &ModelHandle)>().iter() { + let model = match assets.get_model(raidillon_core::ModelId(mh.0)) { + Some(model) => model, + None => { + eprintln!("[renderer] Model with ID {} not found in assets", mh.0); + continue; + } + }; + let mesh = &model.mesh; + let mat = &model.material; + + let tex_ref: &SrgbTexture2d = mat.base_color.as_ref().unwrap_or(&self.white_tex); + + let mut sampler = tex_ref.sampled(); + sampler = sampler.wrap_function(SamplerWrapFunction::Repeat); + sampler = sampler.minify_filter(MinifySamplerFilter::Linear); + sampler = sampler.magnify_filter(MagnifySamplerFilter::Linear); + + let c = mat.base_color_factor; + + let uniforms = uniform! { + model: tr.matrix().to_cols_array_2d(), + view: cam.view().to_cols_array_2d(), + projection: cam.projection().to_cols_array_2d(), + u_light: [light_dir.x, light_dir.y, light_dir.z], + tex: sampler, + color: [c[0], c[1], c[2]], + uv_offset: [mat.uv_offset.x, mat.uv_offset.y], + uv_scale: [mat.uv_scale.x, mat.uv_scale.y], + }; + + target.draw( + &mesh.vbuf, + &mesh.ibuf, + &self.program, + &uniforms, + &self.params, + ).unwrap(); + } + + // Render skybox + let mut sky_view = cam.view(); + sky_view.w_axis = Vec4::new(0.0, 0.0, 0.0, 1.0); + + let mut sampler = self.skybox_texture.sampled(); + sampler = sampler.wrap_function(SamplerWrapFunction::Clamp); + sampler = sampler.minify_filter(MinifySamplerFilter::Linear); + sampler = sampler.magnify_filter(MagnifySamplerFilter::Linear); + + let uniforms = uniform! { + view: sky_view.to_cols_array_2d(), + projection: cam.projection().to_cols_array_2d(), + equirect: sampler, + }; + + let sky_params = glium::DrawParameters { + depth: glium::Depth { + test: DepthTest::IfLessOrEqual, + write: false, + .. Default::default() + }, + .. Default::default() + }; + + target.draw( + &self.skybox_mesh.vbuf, + &self.skybox_mesh.ibuf, + &self.skybox_program, + &uniforms, + &sky_params, + ).unwrap(); + } } diff --git a/raidillon_render/src/render_system.rs b/raidillon_render/src/render_system.rs new file mode 100644 index 0000000..7c3a468 --- /dev/null +++ b/raidillon_render/src/render_system.rs @@ -0,0 +1,50 @@ +use hecs::World; +use raidillon_core::{AssetManager, ModelId}; +use crate::render::GliumRenderer; +use crate::model::{Model, Material}; +use crate::window::DisplayHandle; +use glium::Surface; + +/// A pure render system that doesn't own the ECS world. +/// This decouples rendering from ECS world ownership. +pub struct RenderSystem { + renderer: GliumRenderer, + assets: AssetManager, +} + +impl RenderSystem { + pub fn new(display: DisplayHandle) -> anyhow::Result { + Ok(Self { + renderer: GliumRenderer::new(display.as_inner().clone())?, + assets: AssetManager::new(), + }) + } + + pub fn render(&mut self, world: &World, target: &mut impl Surface) { + // Pass the asset manager to the renderer for accessing models + self.renderer.render_into_with_assets(world, &self.assets, target); + } + + pub fn load_model(&mut self, path: &str) -> anyhow::Result { + // Check cache first + let model = crate::gltf_loader::load_gltf(path, self.renderer.display())?; + let model_id = self.assets.cache_model(path.to_string(), Box::new(model)); + Ok(model_id) + } + + pub fn display(&self) -> &glium::Display { + self.renderer.display() + } + + pub fn get_model(&self, id: ModelId) -> Option<&Model> { + self.assets.get_model(id) + } + + pub fn assets(&self) -> &AssetManager { + &self.assets + } + + pub fn assets_mut(&mut self) -> &mut AssetManager { + &mut self.assets + } +} \ No newline at end of file