Skip to content

Commit d30fe8c

Browse files
NathanSWardostwilkens
authored andcommitted
[ecs] implement is_empty for queries (bevyengine#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: bevyengine#2270 ## Solution - Implement an `is_empty` function for queries to more easily check if the query is empty.
1 parent d872173 commit d30fe8c

File tree

4 files changed

+99
-0
lines changed

4 files changed

+99
-0
lines changed

crates/bevy_ecs/src/query/iter.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,62 @@ where
6969
current_index: 0,
7070
}
7171
}
72+
73+
/// Consumes `self` and returns true if there were no elements remaining in this iterator.
74+
#[inline(always)]
75+
pub(crate) fn none_remaining(mut self) -> bool {
76+
// NOTE: this mimics the behavior of `QueryIter::next()`, except that it
77+
// never gets a `Self::Item`.
78+
unsafe {
79+
if self.is_dense {
80+
loop {
81+
if self.current_index == self.current_len {
82+
let table_id = match self.table_id_iter.next() {
83+
Some(table_id) => table_id,
84+
None => return true,
85+
};
86+
let table = &self.tables[*table_id];
87+
self.filter.set_table(&self.query_state.filter_state, table);
88+
self.current_len = table.len();
89+
self.current_index = 0;
90+
continue;
91+
}
92+
93+
if !self.filter.table_filter_fetch(self.current_index) {
94+
self.current_index += 1;
95+
continue;
96+
}
97+
98+
return false;
99+
}
100+
} else {
101+
loop {
102+
if self.current_index == self.current_len {
103+
let archetype_id = match self.archetype_id_iter.next() {
104+
Some(archetype_id) => archetype_id,
105+
None => return true,
106+
};
107+
let archetype = &self.archetypes[*archetype_id];
108+
self.filter.set_archetype(
109+
&self.query_state.filter_state,
110+
archetype,
111+
self.tables,
112+
);
113+
self.current_len = archetype.len();
114+
self.current_index = 0;
115+
continue;
116+
}
117+
118+
if !self.filter.archetype_filter_fetch(self.current_index) {
119+
self.current_index += 1;
120+
continue;
121+
}
122+
123+
return false;
124+
}
125+
}
126+
}
127+
}
72128
}
73129

74130
impl<'w, 's, Q: WorldQuery, F: WorldQuery> Iterator for QueryIter<'w, 's, Q, F>

crates/bevy_ecs/src/query/state.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ where
6868
state
6969
}
7070

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

crates/bevy_ecs/src/system/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,30 @@ mod tests {
438438
assert_eq!(conflicts, vec![b_id, d_id]);
439439
}
440440

441+
#[test]
442+
fn query_is_empty() {
443+
fn without_filter(not_empty: Query<&A>, empty: Query<&B>) {
444+
assert!(!not_empty.is_empty());
445+
assert!(empty.is_empty());
446+
}
447+
448+
fn with_filter(not_empty: Query<&A, With<C>>, empty: Query<&A, With<D>>) {
449+
assert!(!not_empty.is_empty());
450+
assert!(empty.is_empty());
451+
}
452+
453+
let mut world = World::default();
454+
world.spawn().insert(A).insert(C);
455+
456+
let mut without_filter = without_filter.system();
457+
without_filter.initialize(&mut world);
458+
without_filter.run((), &mut world);
459+
460+
let mut with_filter = with_filter.system();
461+
with_filter.initialize(&mut world);
462+
with_filter.run((), &mut world);
463+
}
464+
441465
#[test]
442466
#[allow(clippy::too_many_arguments)]
443467
fn can_have_16_parameters() {

crates/bevy_ecs/src/system/query.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,15 @@ where
543543
>())),
544544
}
545545
}
546+
547+
/// Returns true if this query contains no elements.
548+
#[inline]
549+
pub fn is_empty(&self) -> bool {
550+
// TODO: This code can be replaced with `self.iter().next().is_none()` if/when
551+
// we sort out how to convert "write" queries to "read" queries.
552+
self.state
553+
.is_empty(self.world, self.last_change_tick, self.change_tick)
554+
}
546555
}
547556

548557
/// An error that occurs when retrieving a specific [`Entity`]'s component from a [`Query`]

0 commit comments

Comments
 (0)