-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Add a limited Not
query filter
#11116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice stuff. I'm pleased to see this work out, and I'm fine with the limitation.
Requests:
- Link to de Morgan's laws.
- Make
InvertibleFilter
pub, with appropriate docs. - Add a doc test to
Changed
filter, showing how to reproduce the "changed but not added" behavior thatMutated
used to provide.
if T::IS_ARCHETYPAL { | ||
!T::matches_component_set(state, set_contains_id) | ||
} else { | ||
T::matches_component_set(state, set_contains_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this a bit questionable since it means that Or<(Added<A>, Not<Added<A>>)
won't match every entity, but only those that have A
. This is also the fundamental problem you face when filter being negated is a tuple mixing With
/Without
and Added
/Changed
. In one case you want to negate the archetypes being matched, in the other you want to negate the filter_fetch
, but in case of tuples you need a mix of them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that it makes sense for Not<Changed<A>>
to also match entities that do not have A
at all. It's not the correct logical negation, but is probably the expected result when filtering on Not<Changed<A>>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I agree that the expectation is different, it also means that the operation that Not
is doing is different for different types, and this can break all sorts of expectations in the code.
Here's yet another test case that fails:
#[test]
fn query_filter_not_broken() {
use crate::system::Query;
fn system(
q1: Query<(Entity, &mut A), Not<Changed<B>>>,
q2: Query<(Entity, &mut A), With<B>>,
) {
for (e1, _) in &q1 {
for (e2, _) in &q2 {
assert_ne!(e1, e2);
}
}
}
let mut world = World::default();
world.spawn((A(0), B(0)));
let system_id = world.register_system(system);
_ = world.run_system(system_id);
_ = world.run_system(system_id);
}
The problem is that you always invert the component access, but then when setting which component sets are matched by Not<Changed<B>>
you don't invert them, hence declaring that Not<Changed<B>>
accesses only archetypes without B
(making it disjoint from the second query) but in the end accessing only those that have B
(making it conflicting with the second query).
If I had to generalize the operations Not
actually does:
- there is an "applicable components set" on which a filter can be applied on. Every archetype not matching it is always excluded by it. For
With
/Without
this set contains every possible component set, but forAdded<T>
/Changed<T>
this contains only component sets containingT
s. This is not inverted byNot
, and should be kept as is. - there is a "filtered components set" which is a subset of the applicable components set that the filter can reduce to. For example
With<T>
/Without<T>
can reduce the applicable components set to only those containingT
or not.Not
inverts this filter. - there is a "fetch filter" that filters individual entities matching the filtered components set. For example
Added<T>
/Changed<T>
apply filters at this point.Not
also inverts this filter.
The problem with this approach is that it adds yet another level of component access/filtering, complicating WorldQuery
even more. But without it I don't think you can reasonably support inverting both With
/Without
and Changed
/Added
without bugs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Not<Changed<T>>
should include entities which do not contain a T
. I think the problem here is the wording between Not Changed T
and an Unchanged T
. Unchanged T
implies the existence of T
, whereas Not Changed T
is strictly the inverse of Changed T
.
A resolution to this problem could be to define:
pub type Unchanged<T> = (With<T>, Not<Changed<T>>);
pub type Existing<T> = (With<T>, Not<Added<T>>);
With some documentation explaining why it's not as simple as Not<Changed<T>>
. For something as fundamental as a Not
operation, it should be identical to the laws of boolean algebra.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still not convinced this is correct, although I don't think it can show misbehaviour with the current implementation (I think it would if Not<(F1, F2, F3,)>
was allowed).
crates/bevy_ecs/src/query/filter.rs
Outdated
for i in 0..intermediate.filter_sets.len() { | ||
for j in i..intermediate.filter_sets.len() { | ||
let filter_a = &intermediate.filter_sets[i]; | ||
let filter_b = &intermediate.filter_sets[j]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I said cartesian product I meant between all the filter sets in intermediate
, not between each pair of them. That is, if the formula that the access expresses is (A & B) | (C & D) | (E & F)
and you want to negate it, you need one atom for each conjuction to be false, so:
(!A & !C & !E) | (!A & !C & !F) | (!A & !D & !E) | (!A & !D & !F) | (!B & !C & !E) | (!B & !C & !F) | (!B & !D & !E) | (!B & !D & !F)
while I expect your code to produce something like this (not tested though):
(!A & !A) | (!A & !B) | (!A & !C) | (!A & !D) | (!B & !A) | (!B & !B) | (!B & !C) | (!B & !D) | (!A & !E) | (!A & !F) | (!B & !E) | (!B & !F) | (!C & !C) | (!C & !D) | (!C & !E) | (!C & !F) | (!D & !C) | (!D & !D) | (!D & !E) | (!D & !F) | (!E & !E) | (!E & !F) | (!F & !E) | (!F & !F)
In a more generic way, you need to compute access & !intermediate
. Expanding them we get something in the following shape ((A1 & ...) | (A2 & ...) | ...) & !((I1 & ...) | (I2 & ...) | ...)
. We can expand the !
in front of the intermediate
part to get ((A1 & ...) | (A2 & ...) | ...) & !(I1 & ...) & !(I2 & ...) & !...
, which means it is a repeated AND with the negation of each filter set in intermediate
, which in turn can be computed by observing that !(I1 & ...)
is equal to !I1 | !...
.
crates/bevy_ecs/src/query/filter.rs
Outdated
let mut inverted = FilteredAccess::<ComponentId>::default(); | ||
inverted.and_without(ComponentId::new(a)); | ||
inverted.and_without(ComponentId::new(b)); | ||
access.append_or(&inverted); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
access.append_or(&inverted)
is essentially equal to access = access OR inverted
, but you need to compute an AND
instead because we're adding restrictions, not alternatives to the existing restrictions. I would use access.extend(&inverted)
instead.
crates/bevy_ecs/src/query/filter.rs
Outdated
for i in 0..intermediate.filter_sets.len() { | ||
for j in i..intermediate.filter_sets.len() { | ||
let filter_a = &intermediate.filter_sets[i]; | ||
let filter_b = &intermediate.filter_sets[j]; | ||
|
||
for a in filter_a.with.ones() { | ||
for b in filter_b.with.ones() { | ||
let mut inverted = FilteredAccess::<ComponentId>::default(); | ||
inverted.and_without(ComponentId::new(a)); | ||
inverted.and_without(ComponentId::new(b)); | ||
access.append_or(&inverted); | ||
} | ||
for b in filter_b.without.ones() { | ||
// with & without the same component is always false, so don't add it | ||
if a == b { | ||
continue; | ||
} | ||
let mut inverted = FilteredAccess::<ComponentId>::default(); | ||
inverted.and_without(ComponentId::new(a)); | ||
inverted.and_with(ComponentId::new(b)); | ||
access.append_or(&inverted); | ||
} | ||
} | ||
|
||
for a in filter_a.without.ones() { | ||
for b in filter_b.with.ones() { | ||
// with & without the same component is always false, so don't add it | ||
if a == b { | ||
continue; | ||
} | ||
let mut inverted = FilteredAccess::<ComponentId>::default(); | ||
inverted.and_with(ComponentId::new(a)); | ||
inverted.and_without(ComponentId::new(b)); | ||
access.append_or(&inverted); | ||
} | ||
for b in filter_b.without.ones() { | ||
let mut inverted = FilteredAccess::<ComponentId>::default(); | ||
inverted.and_with(ComponentId::new(a)); | ||
inverted.and_with(ComponentId::new(b)); | ||
access.append_or(&inverted); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also move all of this into a method on FilteredAccessSet
Without being able to use |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also see my comment here on why this is still unsound.
archetype: &Archetype, | ||
access: &mut Access<ArchetypeComponentId>, | ||
) { | ||
T::update_archetype_component_access(state, archetype, access); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't find time to look for a failing example, but this feels wrong, it should be inverted (but again only for archetypal filters...)
pub fn invert_filters(&mut self) { | ||
let filter_sets = self.filter_sets.drain(..); | ||
|
||
#[derive(Debug, Copy, Clone)] | ||
enum AccessType { | ||
With(usize), | ||
Without(usize), | ||
} | ||
|
||
// Gather and unify each filter set from separate bitsets for With and Without into | ||
// a single collection to simplify logic | ||
let mut accesses = vec![]; | ||
for filter in filter_sets { | ||
let mut filters = vec![]; | ||
filters.extend(filter.with.ones().map(AccessType::With).collect::<Vec<_>>()); | ||
filters.extend( | ||
filter | ||
.without | ||
.ones() | ||
.map(AccessType::Without) | ||
.collect::<Vec<_>>(), | ||
); | ||
accesses.push(filters); | ||
} | ||
|
||
let mut indices = vec![0; accesses.len()]; | ||
let lengths = accesses.iter().map(|v| v.len()).collect::<Vec<_>>(); | ||
|
||
loop { | ||
let mut new_filter = AccessFilters::default(); | ||
for (el, index) in indices.iter().enumerate() { | ||
match accesses[el][*index] { | ||
AccessType::With(id) => { | ||
new_filter.without.grow(id + 1); | ||
new_filter.without.insert(id); | ||
} | ||
AccessType::Without(id) => { | ||
new_filter.with.grow(id + 1); | ||
new_filter.with.insert(id); | ||
} | ||
} | ||
} | ||
self.filter_sets.push(new_filter); | ||
|
||
let mut update_index = indices.len() - 1; | ||
loop { | ||
// update while rolling over indices | ||
indices[update_index] = (indices[update_index] + 1) % lengths[update_index]; | ||
|
||
// if index did not roll over, or we are at the first element, stop looping | ||
if indices[update_index] != 0 || update_index == 0 { | ||
break; | ||
} | ||
|
||
update_index -= 1; | ||
} | ||
|
||
// if we have rolled over the first element, we have gone through all combinations | ||
if update_index == 0 && indices[0] == 0 { | ||
break; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels way more complicated than what I had imagined. I was thinking of something like this:
pub fn invert_filters(&mut self) {
let mut acc = FilteredAccess::default();
for filter_set in self.filter_sets.drain(..) {
let mut new = FilteredAccess::default();
for with in filter_set.with.ones() {
let mut tmp = FilteredAccess::default();
tmp.and_without(with);
new.append_or(&tmp);
}
for without in filter_set.with.ones() {
let mut tmp = FilteredAccess::default();
tmp.and_with(without);
new.append_or(&tmp);
}
acc.extend(&new);
}
*self = acc;
}
The idea is that !((... & ...) | (... & ...) | ...) = !(... & ...) & !(... & ...) & ... = (!... | !...) & (!... | !...) & ...
, so you just need to invert the singlular terms in each conjunction (using and_without
for the existing with
and and_with
for the existing without
), combine them in a disjunction (using append_or
, but could be done more efficiently) and then combine everything together in a conjunction (using extend
).
I'm also not sure what should be done with self.accesses
(not to be confused with your accessed
local variable)
Objective
Resolve #7265.
Solution
Create a
Not
query filter, inverting primitive query filters.Implementation note and future work
This implementation only allows for inversions of
Added<T>
,Changed<T>
,With<T>
andWithout<T>
. The implementation without the addedInvertibleFilter
trait produces the correct result for tuples (andOr
tuples) of only archetypal filters and only non-archetypal filters, but not tuples that mix the two. I was unable to figure out how to distinguish these cases, and chose to omit tuples altogether for correctness since it does not affect the possible filters that can be expressed.Changelog
Add a
Not
query filter, inverting the condition of primitive query filters.