feat(ecs): add tick-based change detection with query_changed
Add per-component tick tracking to SparseSet. Insert and get_mut mark the current tick; increment_tick advances it. World gains query_changed to find entities whose component changed this tick, and clear_changed to advance all storages at end of frame. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,8 @@ pub struct SparseSet<T> {
|
|||||||
sparse: Vec<Option<usize>>,
|
sparse: Vec<Option<usize>>,
|
||||||
dense_entities: Vec<Entity>,
|
dense_entities: Vec<Entity>,
|
||||||
dense_data: Vec<T>,
|
dense_data: Vec<T>,
|
||||||
|
ticks: Vec<u64>,
|
||||||
|
current_tick: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> SparseSet<T> {
|
impl<T> SparseSet<T> {
|
||||||
@@ -13,6 +15,8 @@ impl<T> SparseSet<T> {
|
|||||||
sparse: Vec::new(),
|
sparse: Vec::new(),
|
||||||
dense_entities: Vec::new(),
|
dense_entities: Vec::new(),
|
||||||
dense_data: Vec::new(),
|
dense_data: Vec::new(),
|
||||||
|
ticks: Vec::new(),
|
||||||
|
current_tick: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,11 +31,13 @@ impl<T> SparseSet<T> {
|
|||||||
// Overwrite existing
|
// Overwrite existing
|
||||||
self.dense_data[dense_idx] = value;
|
self.dense_data[dense_idx] = value;
|
||||||
self.dense_entities[dense_idx] = entity;
|
self.dense_entities[dense_idx] = entity;
|
||||||
|
self.ticks[dense_idx] = self.current_tick;
|
||||||
} else {
|
} else {
|
||||||
let dense_idx = self.dense_data.len();
|
let dense_idx = self.dense_data.len();
|
||||||
self.sparse[id] = Some(dense_idx);
|
self.sparse[id] = Some(dense_idx);
|
||||||
self.dense_entities.push(entity);
|
self.dense_entities.push(entity);
|
||||||
self.dense_data.push(value);
|
self.dense_data.push(value);
|
||||||
|
self.ticks.push(self.current_tick);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,12 +55,14 @@ impl<T> SparseSet<T> {
|
|||||||
|
|
||||||
if dense_idx == last_idx {
|
if dense_idx == last_idx {
|
||||||
self.dense_entities.pop();
|
self.dense_entities.pop();
|
||||||
|
self.ticks.pop();
|
||||||
Some(self.dense_data.pop().unwrap())
|
Some(self.dense_data.pop().unwrap())
|
||||||
} else {
|
} else {
|
||||||
// Swap with last
|
// Swap with last
|
||||||
let swapped_entity = self.dense_entities[last_idx];
|
let swapped_entity = self.dense_entities[last_idx];
|
||||||
self.sparse[swapped_entity.id as usize] = Some(dense_idx);
|
self.sparse[swapped_entity.id as usize] = Some(dense_idx);
|
||||||
self.dense_entities.swap_remove(dense_idx);
|
self.dense_entities.swap_remove(dense_idx);
|
||||||
|
self.ticks.swap_remove(dense_idx);
|
||||||
Some(self.dense_data.swap_remove(dense_idx))
|
Some(self.dense_data.swap_remove(dense_idx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,6 +82,7 @@ impl<T> SparseSet<T> {
|
|||||||
if self.dense_entities[dense_idx] != entity {
|
if self.dense_entities[dense_idx] != entity {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
self.ticks[dense_idx] = self.current_tick;
|
||||||
Some(&mut self.dense_data[dense_idx])
|
Some(&mut self.dense_data[dense_idx])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +123,29 @@ impl<T> SparseSet<T> {
|
|||||||
pub fn data_mut(&mut self) -> &mut [T] {
|
pub fn data_mut(&mut self) -> &mut [T] {
|
||||||
&mut self.dense_data
|
&mut self.dense_data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if an entity's component was changed this tick.
|
||||||
|
pub fn is_changed(&self, entity: Entity) -> bool {
|
||||||
|
if let Some(&index) = self.sparse.get(entity.id as usize).and_then(|o| o.as_ref()) {
|
||||||
|
self.ticks[index] == self.current_tick
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advance the tick counter (call at end of frame).
|
||||||
|
pub fn increment_tick(&mut self) {
|
||||||
|
self.current_tick += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return entities changed this tick with their data.
|
||||||
|
pub fn iter_changed(&self) -> impl Iterator<Item = (Entity, &T)> + '_ {
|
||||||
|
self.dense_entities.iter()
|
||||||
|
.zip(self.dense_data.iter())
|
||||||
|
.zip(self.ticks.iter())
|
||||||
|
.filter(move |((_, _), &tick)| tick == self.current_tick)
|
||||||
|
.map(|((entity, data), _)| (*entity, data))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Default for SparseSet<T> {
|
impl<T> Default for SparseSet<T> {
|
||||||
@@ -127,6 +159,7 @@ pub trait ComponentStorage: Any {
|
|||||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||||
fn remove_entity(&mut self, entity: Entity);
|
fn remove_entity(&mut self, entity: Entity);
|
||||||
fn storage_len(&self) -> usize;
|
fn storage_len(&self) -> usize;
|
||||||
|
fn increment_tick(&mut self);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: 'static> ComponentStorage for SparseSet<T> {
|
impl<T: 'static> ComponentStorage for SparseSet<T> {
|
||||||
@@ -142,6 +175,9 @@ impl<T: 'static> ComponentStorage for SparseSet<T> {
|
|||||||
fn storage_len(&self) -> usize {
|
fn storage_len(&self) -> usize {
|
||||||
self.dense_data.len()
|
self.dense_data.len()
|
||||||
}
|
}
|
||||||
|
fn increment_tick(&mut self) {
|
||||||
|
self.current_tick += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -235,6 +271,74 @@ mod tests {
|
|||||||
assert!(!set.contains(e));
|
assert!(!set.contains(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_insert_is_changed() {
|
||||||
|
let mut set = SparseSet::<u32>::new();
|
||||||
|
let e = make_entity(0, 0);
|
||||||
|
set.insert(e, 42);
|
||||||
|
assert!(set.is_changed(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_mut_marks_changed() {
|
||||||
|
let mut set = SparseSet::<u32>::new();
|
||||||
|
let e = make_entity(0, 0);
|
||||||
|
set.insert(e, 42);
|
||||||
|
set.increment_tick();
|
||||||
|
assert!(!set.is_changed(e));
|
||||||
|
let _ = set.get_mut(e);
|
||||||
|
assert!(set.is_changed(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_not_changed() {
|
||||||
|
let mut set = SparseSet::<u32>::new();
|
||||||
|
let e = make_entity(0, 0);
|
||||||
|
set.insert(e, 42);
|
||||||
|
set.increment_tick();
|
||||||
|
let _ = set.get(e);
|
||||||
|
assert!(!set.is_changed(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clear_resets_changed() {
|
||||||
|
let mut set = SparseSet::<u32>::new();
|
||||||
|
let e = make_entity(0, 0);
|
||||||
|
set.insert(e, 42);
|
||||||
|
assert!(set.is_changed(e));
|
||||||
|
set.increment_tick();
|
||||||
|
assert!(!set.is_changed(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_iter_changed() {
|
||||||
|
let mut set = SparseSet::<u32>::new();
|
||||||
|
let e1 = make_entity(0, 0);
|
||||||
|
let e2 = make_entity(1, 0);
|
||||||
|
let e3 = make_entity(2, 0);
|
||||||
|
set.insert(e1, 10);
|
||||||
|
set.insert(e2, 20);
|
||||||
|
set.insert(e3, 30);
|
||||||
|
set.increment_tick();
|
||||||
|
let _ = set.get_mut(e2);
|
||||||
|
let changed: Vec<_> = set.iter_changed().collect();
|
||||||
|
assert_eq!(changed.len(), 1);
|
||||||
|
assert_eq!(changed[0].0.id, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_remove_preserves_ticks() {
|
||||||
|
let mut set = SparseSet::<u32>::new();
|
||||||
|
let e1 = make_entity(0, 0);
|
||||||
|
let e2 = make_entity(1, 0);
|
||||||
|
set.insert(e1, 10);
|
||||||
|
set.insert(e2, 20);
|
||||||
|
set.increment_tick();
|
||||||
|
let _ = set.get_mut(e2);
|
||||||
|
set.remove(e1);
|
||||||
|
assert!(set.is_changed(e2));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_swap_remove_correctness() {
|
fn test_swap_remove_correctness() {
|
||||||
let mut set: SparseSet<i32> = SparseSet::new();
|
let mut set: SparseSet<i32> = SparseSet::new();
|
||||||
|
|||||||
@@ -228,6 +228,23 @@ impl World {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Query entities whose component T was changed this tick.
|
||||||
|
pub fn query_changed<T: 'static>(&self) -> Vec<(Entity, &T)> {
|
||||||
|
if let Some(storage) = self.storages.get(&TypeId::of::<T>()) {
|
||||||
|
let set = storage.as_any().downcast_ref::<SparseSet<T>>().unwrap();
|
||||||
|
set.iter_changed().collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advance tick on all component storages (call at end of frame).
|
||||||
|
pub fn clear_changed(&mut self) {
|
||||||
|
for storage in self.storages.values_mut() {
|
||||||
|
storage.increment_tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn has_component<T: 'static>(&self, entity: Entity) -> bool {
|
pub fn has_component<T: 'static>(&self, entity: Entity) -> bool {
|
||||||
self.storage::<T>().map_or(false, |s| s.contains(entity))
|
self.storage::<T>().map_or(false, |s| s.contains(entity))
|
||||||
}
|
}
|
||||||
@@ -526,6 +543,37 @@ mod tests {
|
|||||||
assert_eq!(results[0].0, e1);
|
assert_eq!(results[0].0, e1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_query_changed() {
|
||||||
|
let mut world = World::new();
|
||||||
|
let e1 = world.spawn();
|
||||||
|
let e2 = world.spawn();
|
||||||
|
world.add(e1, 10u32);
|
||||||
|
world.add(e2, 20u32);
|
||||||
|
world.clear_changed();
|
||||||
|
if let Some(v) = world.get_mut::<u32>(e1) {
|
||||||
|
*v = 100;
|
||||||
|
}
|
||||||
|
let changed = world.query_changed::<u32>();
|
||||||
|
assert_eq!(changed.len(), 1);
|
||||||
|
assert_eq!(*changed[0].1, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clear_changed_all_storages() {
|
||||||
|
let mut world = World::new();
|
||||||
|
let e = world.spawn();
|
||||||
|
world.add(e, 42u32);
|
||||||
|
world.add(e, 3.14f32);
|
||||||
|
let changed_u32 = world.query_changed::<u32>();
|
||||||
|
assert_eq!(changed_u32.len(), 1);
|
||||||
|
world.clear_changed();
|
||||||
|
let changed_u32 = world.query_changed::<u32>();
|
||||||
|
assert_eq!(changed_u32.len(), 0);
|
||||||
|
let changed_f32 = world.query_changed::<f32>();
|
||||||
|
assert_eq!(changed_f32.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[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