Giant refactor for a better event-driven architecture

This commit is contained in:
reo 2025-07-21 23:52:32 +03:00
parent 341d531db3
commit 88a21040cd
22 changed files with 936 additions and 67 deletions

10
Cargo.lock generated
View file

@ -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",
]

View file

@ -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"

View file

@ -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<M: Model + ?Sized, Mat: Material + ?Sized> {
models: Vec<Box<M>>,
materials: Vec<Box<Mat>>,
textures: HashMap<String, TextureHandle>,
model_cache: HashMap<String, ModelId>,
next_texture_id: usize,
}
impl<M: Model + ?Sized, Mat: Material + ?Sized> AssetManager<M, Mat> {
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<M>) -> ModelId {
let id = ModelId(self.models.len());
self.models.push(model);
id
}
pub fn cache_model(&mut self, path: String, model: Box<M>) -> 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<Mat>) -> 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<TextureHandle> {
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();
}
}

View file

@ -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<dyn Model, dyn Material>,
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<S: crate::System + 'static>(&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<ModelId> {
// 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()
}
}

View file

@ -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<GameEvent>,
handlers: Vec<Box<dyn EventHandler>>,
}
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<H: EventHandler + 'static>(&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
}
}

View file

@ -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;

View file

@ -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<dyn Model, dyn Material>, 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<Box<dyn System>>,
}
impl SystemRegistry {
pub fn new() -> Self {
Self {
systems: Vec::new(),
}
}
pub fn add_system<S: System + 'static>(&mut self, system: S) {
self.systems.push(Box::new(system));
}
pub fn update_all(&mut self, world: &mut World, resources: &AssetManager<dyn Model, dyn Material>, 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();
}
}

View file

@ -13,5 +13,5 @@ impl Transform {
}
}
#[derive(Clone)]
#[derive(Copy, Clone, Debug)]
pub struct ModelHandle(pub usize);

View file

@ -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" }

View file

@ -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"))
}
}
}

View file

@ -0,0 +1,4 @@
pub mod game_state;
pub use game_state::GameState;
pub use raidillon_core::InputAction;

View file

@ -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<Self> {
Ok(Self {
render_system: RenderSystem::new(display)?,
})
}
fn load_model(&mut self, path: &str) -> anyhow::Result<raidillon_core::ModelId> {
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<glium::glutin::surface::WindowSurface> {
self.render_system.display()
}
}
impl System for RenderSystemWrapper {
fn update(&mut self, _world: &mut World, _resources: &AssetManager<dyn Model, dyn Material>, _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::<Action>::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();
}

View file

@ -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");
}

View file

@ -5,4 +5,7 @@ edition = "2021"
[dependencies]
winit = "0.30"
glam = "0.30.4"
glam = "0.30.4"
hecs = "0.10.5"
raidillon_core = { path = "../raidillon_core" }
raidillon_render = { path = "../raidillon_render" }

View file

@ -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<dyn Model, dyn Material>, _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"
}
}

View file

@ -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<InputAction>,
}
impl InputSystem {
pub fn new() -> Self {
let mut input = Input::<InputAction>::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<T>(&mut self, event: &winit::event::Event<T>) {
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<dyn Model, dyn Material>, _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"
}
}

View file

@ -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<A: Copy + Eq + Hash> {
pressed_keys: HashSet<KeyCode>,

View file

@ -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"

View file

@ -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};

View file

@ -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 {}

View file

@ -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<WindowSurface> {
&self.display
}
pub fn render_into_with_assets<S: Surface>(&mut self, world: &World, assets: &AssetManager<Model, Material>, 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<S: Surface>(&mut self, world: &World, assets: &AssetManager<Model, Material>, 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();
}
}

View file

@ -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<Model, Material>,
}
impl RenderSystem {
pub fn new(display: DisplayHandle) -> anyhow::Result<Self> {
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<ModelId> {
// 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<glium::glutin::surface::WindowSurface> {
self.renderer.display()
}
pub fn get_model(&self, id: ModelId) -> Option<&Model> {
self.assets.get_model(id)
}
pub fn assets(&self) -> &AssetManager<Model, Material> {
&self.assets
}
pub fn assets_mut(&mut self) -> &mut AssetManager<Model, Material> {
&mut self.assets
}
}