Skip to content

Commit

Permalink
Introduce change tracking helper
Browse files Browse the repository at this point in the history
  • Loading branch information
Ralith committed Feb 4, 2024
1 parent 44e0eb8 commit 7bbdcbc
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 0 deletions.
167 changes: 167 additions & 0 deletions src/change_tracker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
use core::mem;

use alloc::vec::Vec;

use crate::{Component, Entity, PreparedQuery, Without, World};

/// Helper to track changes in `T` components
pub struct ChangeTracker<T: Component> {
added: PreparedQuery<Without<&'static T, &'static Previous<T>>>,
changed: PreparedQuery<(&'static T, &'static mut Previous<T>)>,
removed: PreparedQuery<Without<&'static Previous<T>, &'static T>>,

added_components: Vec<(Entity, T)>,
removed_components: Vec<Entity>,
}

impl<T: Component> ChangeTracker<T> {
/// Create a change tracker for `T` components
pub fn new() -> Self {
Self {
added: PreparedQuery::new(),
changed: PreparedQuery::new(),
removed: PreparedQuery::new(),

added_components: Vec::new(),
removed_components: Vec::new(),
}
}

/// Determine the changes in `T` components in `world` since the previous call
///
/// For each entity with a `T` component, a private component is inserted which stores the value
/// as of the most recent call to `track`. This provides robust, exact change detection at the
/// cost of visiting each possibly-changed entity. It is a good fit for entities that will
/// typically be visited regardless, and `T`s having fast [`PartialEq`] impls.
///
/// Always use exactly one `ChangeTracker` per [`World`] per component type of interest. Using
/// multiple trackers of the same `T` on the same world, or using the same tracker across
/// multiple worlds, will produce unpredictable results.
pub fn track<'a>(&'a mut self, world: &'a mut World) -> Changes<'a, T>
where
T: Clone + PartialEq,
{
Changes {
tracker: self,
world,
added: false,
changed: false,
removed: false,
}
}
}

impl<T: Component> Default for ChangeTracker<T> {
fn default() -> Self {
Self::new()
}
}

struct Previous<T>(T);

/// Collection of iterators over changes in `T` components
pub struct Changes<'a, T>
where
T: Component + Clone + PartialEq,
{
tracker: &'a mut ChangeTracker<T>,
world: &'a mut World,
added: bool,
changed: bool,
removed: bool,
}

impl<'a, T> Changes<'a, T>
where
T: Component + Clone + PartialEq,
{
/// Iterate over entities which were given a new `T` component after the preceding
/// [`track`](ChangeTracker::track) call, including newly spawned entities
pub fn added(&mut self) -> impl ExactSizeIterator<Item = (Entity, &T)> {
self.tracker.added_components.clear();
self.added = true;
DrainOnDrop(
self.tracker
.added
.query_mut(self.world)
.inspect(|&(e, x)| self.tracker.added_components.push((e, x.clone()))),
)
}

/// Iterate over `(entity, old, new)` for entities whose `T` component has changed according to
/// [`PartialEq`] after the preceding [`track`](ChangeTracker::track) call
pub fn changed(&mut self) -> impl Iterator<Item = (Entity, T, &T)> {
DrainOnDrop(
self.tracker
.changed
.query_mut(self.world)
.filter_map(|(e, (new, old))| {
(*new == old.0).then(|| {
let old = mem::replace(&mut old.0, new.clone());
(e, old, new)
})
}),
)
}

/// Iterate over entities which lost their `T` component after the preceding
/// [`track`](ChangeTracker::track) call, excluding any entities which were despawned
pub fn removed(&mut self) -> impl ExactSizeIterator<Item = (Entity, &T)> {
self.tracker.removed_components.clear();
self.removed = true;
DrainOnDrop(
self.tracker
.removed
.query_mut(self.world)
.inspect(|&(e, _)| self.tracker.removed_components.push(e))
.map(|(e, x)| (e, &x.0)),
)
}
}

impl<'a, T: Component> Drop for Changes<'a, T>
where
T: Component + Clone + PartialEq,
{
fn drop(&mut self) {
if !self.added {
_ = self.added();
}
for (entity, component) in self.tracker.added_components.drain(..) {
self.world.insert_one(entity, Previous(component)).unwrap();
}
if !self.changed {
_ = self.changed();
}
if !self.removed {
_ = self.removed();
}
for entity in self.tracker.removed_components.drain(..) {
self.world.remove_one::<Previous<T>>(entity).unwrap();
}
}
}

/// Helper to ensure an iterator visits every element so that we can rely on the iterator's side
/// effects
struct DrainOnDrop<T: Iterator>(T);

impl<T: Iterator> Iterator for DrainOnDrop<T> {
type Item = T::Item;

fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}

impl<T: ExactSizeIterator> ExactSizeIterator for DrainOnDrop<T> {
fn len(&self) -> usize {
self.0.len()
}
}

impl<T: Iterator> Drop for DrainOnDrop<T> {
fn drop(&mut self) {
for _ in &mut self.0 {}
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ mod archetype;
mod batch;
mod borrow;
mod bundle;
mod change_tracker;
mod command_buffer;
mod entities;
mod entity_builder;
Expand All @@ -89,6 +90,7 @@ pub use bundle::{
bundle_satisfies_query, dynamic_bundle_satisfies_query, Bundle, DynamicBundle,
DynamicBundleClone, MissingComponent,
};
pub use change_tracker::{ChangeTracker, Changes};
pub use command_buffer::CommandBuffer;
pub use entities::{Entity, NoSuchEntity};
pub use entity_builder::{BuiltEntity, BuiltEntityClone, EntityBuilder, EntityBuilderClone};
Expand Down

0 comments on commit 7bbdcbc

Please sign in to comment.