[ecs] implement is_empty for queries (#2271)

## Problem
- The `Query` struct does not provide an easy way to check if it is empty. 
- Specifically, users have to use `.iter().peekable()` or `.iter().next().is_none()` which is not very ergonomic. 
- Fixes: #2270 

## Solution
- Implement an `is_empty` function for queries to more easily check if the query is empty.
This commit is contained in:
Nathan Ward 2021-06-02 20:50:06 +00:00
parent a20dc36c8c
commit 19db1e402b
4 changed files with 99 additions and 0 deletions

View file

@ -69,6 +69,62 @@ where
current_index: 0,
}
}
/// Consumes `self` and returns true if there were no elements remaining in this iterator.
#[inline(always)]
pub(crate) fn none_remaining(mut self) -> bool {
// NOTE: this mimics the behavior of `QueryIter::next()`, except that it
// never gets a `Self::Item`.
unsafe {
if self.is_dense {
loop {
if self.current_index == self.current_len {
let table_id = match self.table_id_iter.next() {
Some(table_id) => table_id,
None => return true,
};
let table = &self.tables[*table_id];
self.filter.set_table(&self.query_state.filter_state, table);
self.current_len = table.len();
self.current_index = 0;
continue;
}
if !self.filter.table_filter_fetch(self.current_index) {
self.current_index += 1;
continue;
}
return false;
}
} else {
loop {
if self.current_index == self.current_len {
let archetype_id = match self.archetype_id_iter.next() {
Some(archetype_id) => archetype_id,
None => return true,
};
let archetype = &self.archetypes[*archetype_id];
self.filter.set_archetype(
&self.query_state.filter_state,
archetype,
self.tables,
);
self.current_len = archetype.len();
self.current_index = 0;
continue;
}
if !self.filter.archetype_filter_fetch(self.current_index) {
self.current_index += 1;
continue;
}
return false;
}
}
}
}
}
impl<'w, 's, Q: WorldQuery, F: WorldQuery> Iterator for QueryIter<'w, 's, Q, F>

View file

@ -68,6 +68,16 @@ where
state
}
#[inline]
pub fn is_empty(&self, world: &World, last_change_tick: u32, change_tick: u32) -> bool {
// SAFE: the iterator is instantly consumed via `none_remaining` and the implementation of
// `QueryIter::none_remaining` never creates any references to the `<Q::Fetch as Fetch<'w>>::Item`.
unsafe {
self.iter_unchecked_manual(world, last_change_tick, change_tick)
.none_remaining()
}
}
pub fn validate_world_and_update_archetypes(&mut self, world: &World) {
if world.id() != self.world_id {
panic!("Attempted to use {} with a mismatched World. QueryStates can only be used with the World they were created from.",

View file

@ -438,6 +438,30 @@ mod tests {
assert_eq!(conflicts, vec![b_id, d_id]);
}
#[test]
fn query_is_empty() {
fn without_filter(not_empty: Query<&A>, empty: Query<&B>) {
assert!(!not_empty.is_empty());
assert!(empty.is_empty());
}
fn with_filter(not_empty: Query<&A, With<C>>, empty: Query<&A, With<D>>) {
assert!(!not_empty.is_empty());
assert!(empty.is_empty());
}
let mut world = World::default();
world.spawn().insert(A).insert(C);
let mut without_filter = without_filter.system();
without_filter.initialize(&mut world);
without_filter.run((), &mut world);
let mut with_filter = with_filter.system();
with_filter.initialize(&mut world);
with_filter.run((), &mut world);
}
#[test]
#[allow(clippy::too_many_arguments)]
fn can_have_16_parameters() {

View file

@ -543,6 +543,15 @@ where
>())),
}
}
/// Returns true if this query contains no elements.
#[inline]
pub fn is_empty(&self) -> bool {
// TODO: This code can be replaced with `self.iter().next().is_none()` if/when
// we sort out how to convert "write" queries to "read" queries.
self.state
.is_empty(self.world, self.last_change_tick, self.change_tick)
}
}
/// An error that occurs when retrieving a specific [`Entity`]'s component from a [`Query`]