From a080f0608b66db129427025810a95e4146af378c Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Wed, 25 Mar 2026 20:05:02 +0900 Subject: [PATCH] feat(ecs): add query filters (with/without) and system scheduler - has_component helper on World - query_with/query_without for single component + filter - query2_with/query2_without for 2-component + filter - System trait with blanket impl for FnMut(&mut World) - Ordered Scheduler (add/run_all) Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/voltex_ecs/src/lib.rs | 2 + crates/voltex_ecs/src/scheduler.rs | 121 +++++++++++++++++++++++++ crates/voltex_ecs/src/world.rs | 138 +++++++++++++++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 crates/voltex_ecs/src/scheduler.rs diff --git a/crates/voltex_ecs/src/lib.rs b/crates/voltex_ecs/src/lib.rs index b42a6ec..b3a74ac 100644 --- a/crates/voltex_ecs/src/lib.rs +++ b/crates/voltex_ecs/src/lib.rs @@ -5,6 +5,7 @@ pub mod transform; pub mod hierarchy; pub mod world_transform; pub mod scene; +pub mod scheduler; pub use entity::{Entity, EntityAllocator}; pub use sparse_set::SparseSet; @@ -13,3 +14,4 @@ pub use transform::Transform; pub use hierarchy::{Parent, Children, add_child, remove_child, despawn_recursive, roots}; pub use world_transform::{WorldTransform, propagate_transforms}; pub use scene::{Tag, serialize_scene, deserialize_scene}; +pub use scheduler::{Scheduler, System}; diff --git a/crates/voltex_ecs/src/scheduler.rs b/crates/voltex_ecs/src/scheduler.rs new file mode 100644 index 0000000..2b5365e --- /dev/null +++ b/crates/voltex_ecs/src/scheduler.rs @@ -0,0 +1,121 @@ +use crate::World; + +/// A system that can be run on the world. +pub trait System { + fn run(&mut self, world: &mut World); +} + +/// Blanket impl: any FnMut(&mut World) is a System. +impl System for F { + fn run(&mut self, world: &mut World) { + (self)(world); + } +} + +/// Runs registered systems in order. +pub struct Scheduler { + systems: Vec>, +} + +impl Scheduler { + pub fn new() -> Self { + Self { systems: Vec::new() } + } + + /// Add a system. Systems run in the order they are added. + pub fn add(&mut self, system: S) -> &mut Self { + self.systems.push(Box::new(system)); + self + } + + /// Run all systems in registration order. + pub fn run_all(&mut self, world: &mut World) { + for system in &mut self.systems { + system.run(world); + } + } + + /// Number of registered systems. + pub fn len(&self) -> usize { + self.systems.len() + } + + pub fn is_empty(&self) -> bool { + self.systems.is_empty() + } +} + +impl Default for Scheduler { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::World; + + #[derive(Debug, PartialEq)] + struct Counter(u32); + + #[test] + fn test_scheduler_runs_in_order() { + let mut world = World::new(); + let e = world.spawn(); + world.add(e, Counter(0)); + + let mut scheduler = Scheduler::new(); + scheduler.add(|world: &mut World| { + let e = world.query::().next().unwrap().0; + let c = world.get_mut::(e).unwrap(); + c.0 += 1; // 0 -> 1 + }); + scheduler.add(|world: &mut World| { + let e = world.query::().next().unwrap().0; + let c = world.get_mut::(e).unwrap(); + c.0 *= 10; // 1 -> 10 + }); + + scheduler.run_all(&mut world); + + let c = world.get::(e).unwrap(); + assert_eq!(c.0, 10); // proves order: add first, then multiply + } + + #[test] + fn test_scheduler_empty() { + let mut world = World::new(); + let mut scheduler = Scheduler::new(); + scheduler.run_all(&mut world); // should not panic + } + + #[test] + fn test_scheduler_multiple_runs() { + let mut world = World::new(); + let e = world.spawn(); + world.add(e, Counter(0)); + + let mut scheduler = Scheduler::new(); + scheduler.add(|world: &mut World| { + let e = world.query::().next().unwrap().0; + let c = world.get_mut::(e).unwrap(); + c.0 += 1; + }); + + scheduler.run_all(&mut world); + scheduler.run_all(&mut world); + scheduler.run_all(&mut world); + + assert_eq!(world.get::(e).unwrap().0, 3); + } + + #[test] + fn test_scheduler_add_chaining() { + let mut scheduler = Scheduler::new(); + scheduler + .add(|_: &mut World| {}) + .add(|_: &mut World| {}); + assert_eq!(scheduler.len(), 2); + } +} diff --git a/crates/voltex_ecs/src/world.rs b/crates/voltex_ecs/src/world.rs index f761639..0667181 100644 --- a/crates/voltex_ecs/src/world.rs +++ b/crates/voltex_ecs/src/world.rs @@ -227,6 +227,54 @@ impl World { } result } + + pub fn has_component(&self, entity: Entity) -> bool { + self.storage::().map_or(false, |s| s.contains(entity)) + } + + /// Query entities that have component T AND also have component W. + pub fn query_with(&self) -> Vec<(Entity, &T)> { + let t_storage = match self.storage::() { + Some(s) => s, + None => return Vec::new(), + }; + let mut result = Vec::new(); + for (entity, data) in t_storage.iter() { + if self.has_component::(entity) { + result.push((entity, data)); + } + } + result + } + + /// Query entities that have component T but NOT component W. + pub fn query_without(&self) -> Vec<(Entity, &T)> { + let t_storage = match self.storage::() { + Some(s) => s, + None => return Vec::new(), + }; + let mut result = Vec::new(); + for (entity, data) in t_storage.iter() { + if !self.has_component::(entity) { + result.push((entity, data)); + } + } + result + } + + /// Query entities with components A and B, that also have component W. + pub fn query2_with(&self) -> Vec<(Entity, &A, &B)> { + self.query2::().into_iter() + .filter(|(e, _, _)| self.has_component::(*e)) + .collect() + } + + /// Query entities with components A and B, that do NOT have component W. + pub fn query2_without(&self) -> Vec<(Entity, &A, &B)> { + self.query2::().into_iter() + .filter(|(e, _, _)| !self.has_component::(*e)) + .collect() + } } impl Default for World { @@ -388,6 +436,96 @@ mod tests { assert_eq!(results[0].0, e0); } + #[test] + fn test_has_component() { + let mut world = World::new(); + let e = world.spawn(); + world.add(e, Position { x: 1.0, y: 2.0 }); + assert!(world.has_component::(e)); + assert!(!world.has_component::(e)); + } + + #[test] + fn test_query_with() { + let mut world = World::new(); + let e0 = world.spawn(); + let e1 = world.spawn(); + let e2 = world.spawn(); + 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 }); + // e1 has Position but no Velocity + world.add(e2, Position { x: 3.0, y: 0.0 }); + world.add(e2, Velocity { dx: 3.0, dy: 0.0 }); + + let results = world.query_with::(); + assert_eq!(results.len(), 2); + let entities: Vec = results.iter().map(|(e, _)| *e).collect(); + assert!(entities.contains(&e0)); + assert!(entities.contains(&e2)); + assert!(!entities.contains(&e1)); + } + + #[test] + fn test_query_without() { + let mut world = World::new(); + let e0 = world.spawn(); + let e1 = world.spawn(); + let e2 = world.spawn(); + 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 }); + // e1 has Position but no Velocity — should be included + world.add(e2, Position { x: 3.0, y: 0.0 }); + world.add(e2, Velocity { dx: 3.0, dy: 0.0 }); + + let results = world.query_without::(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].0, e1); + } + + #[test] + fn test_query2_with() { + #[derive(Debug, PartialEq)] + struct Health(i32); + + let mut world = World::new(); + let e0 = world.spawn(); + world.add(e0, Position { x: 1.0, y: 0.0 }); + world.add(e0, Velocity { dx: 1.0, dy: 0.0 }); + world.add(e0, Health(100)); + + let e1 = world.spawn(); + world.add(e1, Position { x: 2.0, y: 0.0 }); + world.add(e1, Velocity { dx: 2.0, dy: 0.0 }); + // e1 has no Health + + let results = world.query2_with::(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].0, e0); + } + + #[test] + fn test_query2_without() { + #[derive(Debug, PartialEq)] + struct Health(i32); + + let mut world = World::new(); + let e0 = world.spawn(); + world.add(e0, Position { x: 1.0, y: 0.0 }); + world.add(e0, Velocity { dx: 1.0, dy: 0.0 }); + world.add(e0, Health(100)); + + let e1 = world.spawn(); + world.add(e1, Position { x: 2.0, y: 0.0 }); + world.add(e1, Velocity { dx: 2.0, dy: 0.0 }); + // e1 has no Health + + let results = world.query2_without::(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].0, e1); + } + #[test] fn test_entity_count() { let mut world = World::new();