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:
2026-03-25 20:05:02 +09:00
parent 8abba16137
commit a080f0608b
3 changed files with 261 additions and 0 deletions

View File

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

View 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);
}
}

View File

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