diff --git a/crates/voltex_ecs/src/lib.rs b/crates/voltex_ecs/src/lib.rs index 9c3c17c..e94b7e6 100644 --- a/crates/voltex_ecs/src/lib.rs +++ b/crates/voltex_ecs/src/lib.rs @@ -1,5 +1,9 @@ pub mod entity; pub mod sparse_set; +pub mod world; +pub mod transform; pub use entity::{Entity, EntityAllocator}; pub use sparse_set::SparseSet; +pub use world::World; +pub use transform::Transform; diff --git a/crates/voltex_ecs/src/transform.rs b/crates/voltex_ecs/src/transform.rs new file mode 100644 index 0000000..707230d --- /dev/null +++ b/crates/voltex_ecs/src/transform.rs @@ -0,0 +1,111 @@ +use voltex_math::{Vec3, Mat4}; + +#[derive(Debug, Clone, Copy)] +pub struct Transform { + pub position: Vec3, + pub rotation: Vec3, // euler angles (radians): pitch(x), yaw(y), roll(z) + pub scale: Vec3, +} + +impl Transform { + pub fn new() -> Self { + Self { + position: Vec3::ZERO, + rotation: Vec3::ZERO, + scale: Vec3::ONE, + } + } + + pub fn from_position(position: Vec3) -> Self { + Self { + position, + rotation: Vec3::ZERO, + scale: Vec3::ONE, + } + } + + pub fn from_position_scale(position: Vec3, scale: Vec3) -> Self { + Self { + position, + rotation: Vec3::ZERO, + scale, + } + } + + /// Builds the model matrix: Translation * RotY * RotX * RotZ * Scale + pub fn matrix(&self) -> Mat4 { + let t = Mat4::translation(self.position.x, self.position.y, self.position.z); + let ry = Mat4::rotation_y(self.rotation.y); + let rx = Mat4::rotation_x(self.rotation.x); + let rz = Mat4::rotation_z(self.rotation.z); + let s = Mat4::scale(self.scale.x, self.scale.y, self.scale.z); + t * ry * rx * rz * s + } +} + +impl Default for Transform { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use voltex_math::Vec4; + use std::f32::consts::FRAC_PI_2; + + fn approx_eq(a: f32, b: f32) -> bool { + (a - b).abs() < 1e-5 + } + + #[test] + fn test_identity_transform() { + let t = Transform::new(); + let m = t.matrix(); + // Transform point (1, 2, 3) — should be unchanged + let p = Vec4::new(1.0, 2.0, 3.0, 1.0); + let result = m * p; + assert!(approx_eq(result.x, 1.0), "x: {}", result.x); + assert!(approx_eq(result.y, 2.0), "y: {}", result.y); + assert!(approx_eq(result.z, 3.0), "z: {}", result.z); + assert!(approx_eq(result.w, 1.0), "w: {}", result.w); + } + + #[test] + fn test_translation() { + let t = Transform::from_position(Vec3::new(10.0, 20.0, 30.0)); + let m = t.matrix(); + // Transform origin — should move to (10,20,30) + let p = Vec4::new(0.0, 0.0, 0.0, 1.0); + let result = m * p; + assert!(approx_eq(result.x, 10.0), "x: {}", result.x); + assert!(approx_eq(result.y, 20.0), "y: {}", result.y); + assert!(approx_eq(result.z, 30.0), "z: {}", result.z); + } + + #[test] + fn test_scale() { + let t = Transform::from_position_scale(Vec3::ZERO, Vec3::new(2.0, 3.0, 4.0)); + let m = t.matrix(); + // Scale (1,1,1) to (2,3,4) + let p = Vec4::new(1.0, 1.0, 1.0, 1.0); + let result = m * p; + assert!(approx_eq(result.x, 2.0), "x: {}", result.x); + assert!(approx_eq(result.y, 3.0), "y: {}", result.y); + assert!(approx_eq(result.z, 4.0), "z: {}", result.z); + } + + #[test] + fn test_rotation_y() { + let mut t = Transform::new(); + // 90° Y rotation on (1,0,0) -> approx (0,0,-1) + t.rotation.y = FRAC_PI_2; + let m = t.matrix(); + let p = Vec4::new(1.0, 0.0, 0.0, 1.0); + let result = m * p; + assert!(approx_eq(result.x, 0.0), "x: {}", result.x); + assert!(approx_eq(result.y, 0.0), "y: {}", result.y); + assert!(approx_eq(result.z, -1.0), "z: {}", result.z); + } +} diff --git a/crates/voltex_ecs/src/world.rs b/crates/voltex_ecs/src/world.rs new file mode 100644 index 0000000..d0b0ed3 --- /dev/null +++ b/crates/voltex_ecs/src/world.rs @@ -0,0 +1,237 @@ +use std::any::TypeId; +use std::collections::HashMap; +use crate::entity::{Entity, EntityAllocator}; +use crate::sparse_set::{SparseSet, ComponentStorage}; + +pub struct World { + allocator: EntityAllocator, + storages: HashMap>, +} + +impl World { + pub fn new() -> Self { + Self { + allocator: EntityAllocator::new(), + storages: HashMap::new(), + } + } + + pub fn spawn(&mut self) -> Entity { + self.allocator.allocate() + } + + pub fn despawn(&mut self, entity: Entity) -> bool { + if !self.allocator.deallocate(entity) { + return false; + } + for storage in self.storages.values_mut() { + storage.remove_entity(entity); + } + true + } + + pub fn is_alive(&self, entity: Entity) -> bool { + self.allocator.is_alive(entity) + } + + pub fn entity_count(&self) -> usize { + self.allocator.alive_count() + } + + pub fn add(&mut self, entity: Entity, component: T) { + let type_id = TypeId::of::(); + let storage = self.storages + .entry(type_id) + .or_insert_with(|| Box::new(SparseSet::::new())); + let set = storage.as_any_mut().downcast_mut::>().unwrap(); + set.insert(entity, component); + } + + pub fn get(&self, entity: Entity) -> Option<&T> { + let type_id = TypeId::of::(); + let storage = self.storages.get(&type_id)?; + let set = storage.as_any().downcast_ref::>()?; + set.get(entity) + } + + pub fn get_mut(&mut self, entity: Entity) -> Option<&mut T> { + let type_id = TypeId::of::(); + let storage = self.storages.get_mut(&type_id)?; + let set = storage.as_any_mut().downcast_mut::>()?; + set.get_mut(entity) + } + + pub fn remove(&mut self, entity: Entity) -> Option { + let type_id = TypeId::of::(); + let storage = self.storages.get_mut(&type_id)?; + let set = storage.as_any_mut().downcast_mut::>()?; + set.remove(entity) + } + + pub fn storage(&self) -> Option<&SparseSet> { + let type_id = TypeId::of::(); + let storage = self.storages.get(&type_id)?; + storage.as_any().downcast_ref::>() + } + + pub fn storage_mut(&mut self) -> Option<&mut SparseSet> { + let type_id = TypeId::of::(); + let storage = self.storages.get_mut(&type_id)?; + storage.as_any_mut().downcast_mut::>() + } + + pub fn query(&self) -> impl Iterator { + self.storage::() + .map(|s| s.iter()) + .into_iter() + .flatten() + } + + pub fn query2(&self) -> Vec<(Entity, &A, &B)> { + let a_storage = match self.storage::() { + Some(s) => s, + None => return Vec::new(), + }; + let b_storage = match self.storage::() { + Some(s) => s, + None => return Vec::new(), + }; + + // Iterate the smaller set, look up in the larger + let mut result = Vec::new(); + if a_storage.len() <= b_storage.len() { + for (entity, a) in a_storage.iter() { + if let Some(b) = b_storage.get(entity) { + result.push((entity, a, b)); + } + } + } else { + for (entity, b) in b_storage.iter() { + if let Some(a) = a_storage.get(entity) { + result.push((entity, a, b)); + } + } + } + result + } +} + +impl Default for World { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, PartialEq)] + struct Position { x: f32, y: f32 } + + #[derive(Debug, PartialEq)] + struct Velocity { dx: f32, dy: f32 } + + #[derive(Debug, PartialEq)] + struct Name(String); + + #[test] + fn test_spawn_and_add() { + let mut world = World::new(); + let e = world.spawn(); + world.add(e, Position { x: 1.0, y: 2.0 }); + let pos = world.get::(e).unwrap(); + assert_eq!(pos.x, 1.0); + assert_eq!(pos.y, 2.0); + } + + #[test] + fn test_get_missing() { + let world = World::new(); + let e = Entity { id: 0, generation: 0 }; + assert!(world.get::(e).is_none()); + } + + #[test] + fn test_get_mut() { + let mut world = World::new(); + let e = world.spawn(); + world.add(e, Position { x: 0.0, y: 0.0 }); + { + let pos = world.get_mut::(e).unwrap(); + pos.x = 42.0; + } + assert_eq!(world.get::(e).unwrap().x, 42.0); + } + + #[test] + fn test_remove_component() { + let mut world = World::new(); + let e = world.spawn(); + world.add(e, Position { x: 5.0, y: 6.0 }); + let removed = world.remove::(e); + assert_eq!(removed, Some(Position { x: 5.0, y: 6.0 })); + assert!(world.get::(e).is_none()); + } + + #[test] + fn test_despawn() { + let mut world = World::new(); + let e = world.spawn(); + world.add(e, Position { x: 1.0, y: 2.0 }); + world.add(e, Velocity { dx: 3.0, dy: 4.0 }); + assert!(world.despawn(e)); + assert!(!world.is_alive(e)); + assert!(world.get::(e).is_none()); + assert!(world.get::(e).is_none()); + } + + #[test] + fn test_query_single() { + let mut world = World::new(); + let e0 = world.spawn(); + let e1 = world.spawn(); + let _e2 = world.spawn(); // no Position + world.add(e0, Position { x: 1.0, y: 0.0 }); + world.add(e1, Position { x: 2.0, y: 0.0 }); + + let results: Vec<(Entity, &Position)> = world.query::().collect(); + assert_eq!(results.len(), 2); + let entities: Vec = results.iter().map(|(e, _)| *e).collect(); + assert!(entities.contains(&e0)); + assert!(entities.contains(&e1)); + } + + #[test] + fn test_query2() { + let mut world = World::new(); + let e0 = world.spawn(); + let e1 = world.spawn(); + let e2 = world.spawn(); // only Position, no Velocity + world.add(e0, Position { x: 1.0, y: 0.0 }); + world.add(e0, Velocity { dx: 1.0, dy: 0.0 }); + world.add(e1, Position { x: 2.0, y: 0.0 }); + world.add(e1, Velocity { dx: 2.0, dy: 0.0 }); + world.add(e2, Position { x: 3.0, y: 0.0 }); + + let results = world.query2::(); + assert_eq!(results.len(), 2); + let entities: Vec = results.iter().map(|(e, _, _)| *e).collect(); + assert!(entities.contains(&e0)); + assert!(entities.contains(&e1)); + assert!(!entities.contains(&e2)); + } + + #[test] + fn test_entity_count() { + let mut world = World::new(); + assert_eq!(world.entity_count(), 0); + let e0 = world.spawn(); + let e1 = world.spawn(); + assert_eq!(world.entity_count(), 2); + world.despawn(e0); + assert_eq!(world.entity_count(), 1); + world.despawn(e1); + assert_eq!(world.entity_count(), 0); + } +}