feat(ecs): add query filters (with/without) and system scheduler
- has_component<T> 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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};
|
||||
|
||||
121
crates/voltex_ecs/src/scheduler.rs
Normal file
121
crates/voltex_ecs/src/scheduler.rs
Normal file
@@ -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<F: FnMut(&mut World)> System for F {
|
||||
fn run(&mut self, world: &mut World) {
|
||||
(self)(world);
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs registered systems in order.
|
||||
pub struct Scheduler {
|
||||
systems: Vec<Box<dyn System>>,
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
pub fn new() -> Self {
|
||||
Self { systems: Vec::new() }
|
||||
}
|
||||
|
||||
/// Add a system. Systems run in the order they are added.
|
||||
pub fn add<S: System + 'static>(&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::<Counter>().next().unwrap().0;
|
||||
let c = world.get_mut::<Counter>(e).unwrap();
|
||||
c.0 += 1; // 0 -> 1
|
||||
});
|
||||
scheduler.add(|world: &mut World| {
|
||||
let e = world.query::<Counter>().next().unwrap().0;
|
||||
let c = world.get_mut::<Counter>(e).unwrap();
|
||||
c.0 *= 10; // 1 -> 10
|
||||
});
|
||||
|
||||
scheduler.run_all(&mut world);
|
||||
|
||||
let c = world.get::<Counter>(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::<Counter>().next().unwrap().0;
|
||||
let c = world.get_mut::<Counter>(e).unwrap();
|
||||
c.0 += 1;
|
||||
});
|
||||
|
||||
scheduler.run_all(&mut world);
|
||||
scheduler.run_all(&mut world);
|
||||
scheduler.run_all(&mut world);
|
||||
|
||||
assert_eq!(world.get::<Counter>(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);
|
||||
}
|
||||
}
|
||||
@@ -227,6 +227,54 @@ impl World {
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn has_component<T: 'static>(&self, entity: Entity) -> bool {
|
||||
self.storage::<T>().map_or(false, |s| s.contains(entity))
|
||||
}
|
||||
|
||||
/// Query entities that have component T AND also have component W.
|
||||
pub fn query_with<T: 'static, W: 'static>(&self) -> Vec<(Entity, &T)> {
|
||||
let t_storage = match self.storage::<T>() {
|
||||
Some(s) => s,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
let mut result = Vec::new();
|
||||
for (entity, data) in t_storage.iter() {
|
||||
if self.has_component::<W>(entity) {
|
||||
result.push((entity, data));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Query entities that have component T but NOT component W.
|
||||
pub fn query_without<T: 'static, W: 'static>(&self) -> Vec<(Entity, &T)> {
|
||||
let t_storage = match self.storage::<T>() {
|
||||
Some(s) => s,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
let mut result = Vec::new();
|
||||
for (entity, data) in t_storage.iter() {
|
||||
if !self.has_component::<W>(entity) {
|
||||
result.push((entity, data));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Query entities with components A and B, that also have component W.
|
||||
pub fn query2_with<A: 'static, B: 'static, W: 'static>(&self) -> Vec<(Entity, &A, &B)> {
|
||||
self.query2::<A, B>().into_iter()
|
||||
.filter(|(e, _, _)| self.has_component::<W>(*e))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Query entities with components A and B, that do NOT have component W.
|
||||
pub fn query2_without<A: 'static, B: 'static, W: 'static>(&self) -> Vec<(Entity, &A, &B)> {
|
||||
self.query2::<A, B>().into_iter()
|
||||
.filter(|(e, _, _)| !self.has_component::<W>(*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::<Position>(e));
|
||||
assert!(!world.has_component::<Velocity>(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::<Position, Velocity>();
|
||||
assert_eq!(results.len(), 2);
|
||||
let entities: Vec<Entity> = 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::<Position, Velocity>();
|
||||
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::<Position, Velocity, Health>();
|
||||
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::<Position, Velocity, Health>();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].0, e1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_entity_count() {
|
||||
let mut world = World::new();
|
||||
|
||||
Reference in New Issue
Block a user