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 hierarchy;
|
||||||
pub mod world_transform;
|
pub mod world_transform;
|
||||||
pub mod scene;
|
pub mod scene;
|
||||||
|
pub mod scheduler;
|
||||||
|
|
||||||
pub use entity::{Entity, EntityAllocator};
|
pub use entity::{Entity, EntityAllocator};
|
||||||
pub use sparse_set::SparseSet;
|
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 hierarchy::{Parent, Children, add_child, remove_child, despawn_recursive, roots};
|
||||||
pub use world_transform::{WorldTransform, propagate_transforms};
|
pub use world_transform::{WorldTransform, propagate_transforms};
|
||||||
pub use scene::{Tag, serialize_scene, deserialize_scene};
|
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
|
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 {
|
impl Default for World {
|
||||||
@@ -388,6 +436,96 @@ mod tests {
|
|||||||
assert_eq!(results[0].0, e0);
|
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]
|
#[test]
|
||||||
fn test_entity_count() {
|
fn test_entity_count() {
|
||||||
let mut world = World::new();
|
let mut world = World::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user