Skip to content

Commit ea42d14

Browse files
james-j-obrienhymm
andauthored
Dynamic queries and builder API (#9774)
# Objective Expand the existing `Query` API to support more dynamic use cases i.e. scripting. ## Prior Art - #6390 - #8308 - #10037 ## Solution - Create a `QueryBuilder` with runtime methods to define the set of component accesses for a built query. - Create new `WorldQueryData` implementations `FilteredEntityMut` and `FilteredEntityRef` as variants of `EntityMut` and `EntityRef` that provide run time checked access to the components included in a given query. - Add new methods to `Query` to create "query lens" with a subset of the access of the initial query. ### Query Builder The `QueryBuilder` API allows you to define a query at runtime. At it's most basic use it will simply create a query with the corresponding type signature: ```rust let query = QueryBuilder::<Entity, With<A>>::new(&mut world).build(); // is equivalent to let query = QueryState::<Entity, With<A>>::new(&mut world); ``` Before calling `.build()` you also have the opportunity to add additional accesses and filters. Here is a simple example where we add additional filter terms: ```rust let entity_a = world.spawn((A(0), B(0))).id(); let entity_b = world.spawn((A(0), C(0))).id(); let mut query_a = QueryBuilder::<Entity>::new(&mut world) .with::<A>() .without::<C>() .build(); assert_eq!(entity_a, query_a.single(&world)); ``` This alone is useful in that allows you to decide which archetypes your query will match at runtime. However it is also very limited, consider a case like the following: ```rust let query_a = QueryBuilder::<&A>::new(&mut world) // Add an additional access .data::<&B>() .build(); ``` This will grant the query an additional read access to component B however we have no way of accessing the data while iterating as the type signature still only includes &A. For an even more concrete example of this consider dynamic components: ```rust let query_a = QueryBuilder::<Entity>::new(&mut world) // Adding a filter is easy since it doesn't need be read later .with_id(component_id_a) // How do I access the data of this component? .ref_id(component_id_b) .build(); ``` With this in mind the `QueryBuilder` API seems somewhat incomplete by itself, we need some way method of accessing the components dynamically. So here's one: ### Query Transmutation If the problem is not having the component in the type signature why not just add it? This PR also adds transmute methods to `QueryBuilder` and `QueryState`. Here's a simple example: ```rust world.spawn(A(0)); world.spawn((A(1), B(0))); let mut query = QueryBuilder::<()>::new(&mut world) .with::<B>() .transmute::<&A>() .build(); query.iter(&world).for_each(|a| assert_eq!(a.0, 1)); ``` The `QueryState` and `QueryBuilder` transmute methods look quite similar but are different in one respect. Transmuting a builder will always succeed as it will just add the additional accesses needed for the new terms if they weren't already included. Transmuting a `QueryState` will panic in the case that the new type signature would give it access it didn't already have, for example: ```rust let query = QueryState::<&A, Option<&B>>::new(&mut world); /// This is fine, the access for Option<&A> is less restrictive than &A query.transmute::<Option<&A>>(&world); /// Oh no, this would allow access to &B on entities that might not have it, so it panics query.transmute::<&B>(&world); /// This is right out query.transmute::<&C>(&world); ``` This is quite an appealing API to also have available on `Query` however it does pose one additional wrinkle: In order to to change the iterator we need to create a new `QueryState` to back it. `Query` doesn't own it's own state though, it just borrows it, so we need a place to borrow it from. This is why `QueryLens` exists, it is a place to store the new state so it can be borrowed when you call `.query()` leaving you with an API like this: ```rust fn function_that_takes_a_query(query: &Query<&A>) { // ... } fn system(query: Query<(&A, &B)>) { let lens = query.transmute_lens::<&A>(); let q = lens.query(); function_that_takes_a_query(&q); } ``` Now you may be thinking: Hey, wait a second, you introduced the problem with dynamic components and then described a solution that only works for static components! Ok, you got me, I guess we need a bit more: ### Filtered Entity References Currently the only way you can access dynamic components on entities through a query is with either `EntityMut` or `EntityRef`, however these can access all components and so conflict with all other accesses. This PR introduces `FilteredEntityMut` and `FilteredEntityRef` as alternatives that have additional runtime checking to prevent accessing components that you shouldn't. This way you can build a query with a `QueryBuilder` and actually access the components you asked for: ```rust let mut query = QueryBuilder::<FilteredEntityRef>::new(&mut world) .ref_id(component_id_a) .with(component_id_b) .build(); let entity_ref = query.single(&world); // Returns Some(Ptr) as we have that component and are allowed to read it let a = entity_ref.get_by_id(component_id_a); // Will return None even though the entity does have the component, as we are not allowed to read it let b = entity_ref.get_by_id(component_id_b); ``` For the most part these new structs have the exact same methods as their non-filtered equivalents. Putting all of this together we can do some truly dynamic ECS queries, check out the `dynamic` example to see it in action: ``` Commands: comp, c Create new components spawn, s Spawn entities query, q Query for entities Enter a command with no parameters for usage. > c A, B, C, Data 4 Component A created with id: 0 Component B created with id: 1 Component C created with id: 2 Component Data created with id: 3 > s A, B, Data 1 Entity spawned with id: 0v0 > s A, C, Data 0 Entity spawned with id: 1v0 > q &Data 0v0: Data: [1, 0, 0, 0] 1v0: Data: [0, 0, 0, 0] > q B, &mut Data 0v0: Data: [2, 1, 1, 1] > q B || C, &Data 0v0: Data: [2, 1, 1, 1] 1v0: Data: [0, 0, 0, 0] ``` ## Changelog - Add new `transmute_lens` methods to `Query`. - Add new types `QueryBuilder`, `FilteredEntityMut`, `FilteredEntityRef` and `QueryLens` - `update_archetype_component_access` has been removed, archetype component accesses are now determined by the accesses set in `update_component_access` - Added method `set_access` to `WorldQuery`, this is called before `update_component_access` for queries that have a restricted set of accesses, such as those built by `QueryBuilder` or `QueryLens`. This is primarily used by the `FilteredEntity*` variants and has an empty trait implementation. - Added method `get_state` to `WorldQuery` as a fallible version of `init_state` when you don't have `&mut World` access. ## Future Work Improve performance of `FilteredEntityMut` and `FilteredEntityRef`, currently they have to determine the accesses a query has in a given archetype during iteration which is far from ideal, especially since we already did the work when matching the archetype in the first place. To avoid making more internal API changes I have left it out of this PR. --------- Co-authored-by: Mike Hsu <[email protected]>
1 parent 54a54d4 commit ea42d14

File tree

17 files changed

+1884
-198
lines changed

17 files changed

+1884
-198
lines changed

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1352,6 +1352,17 @@ description = "Groups commonly used compound queries and query filters into a si
13521352
category = "ECS (Entity Component System)"
13531353
wasm = false
13541354

1355+
[[example]]
1356+
name = "dynamic"
1357+
path = "examples/ecs/dynamic.rs"
1358+
doc-scrape-examples = true
1359+
1360+
[package.metadata.example.dynamic]
1361+
name = "Dynamic ECS"
1362+
description = "Dynamically create components, spawn entities with those components and query those components"
1363+
category = "ECS (Entity Component System)"
1364+
wasm = false
1365+
13551366
[[example]]
13561367
name = "event"
13571368
path = "examples/ecs/event.rs"

crates/bevy_ecs/macros/src/world_query.rs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -165,22 +165,18 @@ pub(crate) fn world_query_impl(
165165
#( <#field_types>::update_component_access(&state.#named_field_idents, _access); )*
166166
}
167167

168-
fn update_archetype_component_access(
169-
state: &Self::State,
170-
_archetype: &#path::archetype::Archetype,
171-
_access: &mut #path::query::Access<#path::archetype::ArchetypeComponentId>
172-
) {
173-
#(
174-
<#field_types>::update_archetype_component_access(&state.#named_field_idents, _archetype, _access);
175-
)*
176-
}
177-
178168
fn init_state(world: &mut #path::world::World) -> #state_struct_name #user_ty_generics {
179169
#state_struct_name {
180170
#(#named_field_idents: <#field_types>::init_state(world),)*
181171
}
182172
}
183173

174+
fn get_state(world: &#path::world::World) -> Option<#state_struct_name #user_ty_generics> {
175+
Some(#state_struct_name {
176+
#(#named_field_idents: <#field_types>::get_state(world)?,)*
177+
})
178+
}
179+
184180
fn matches_component_set(state: &Self::State, _set_contains_id: &impl Fn(#path::component::ComponentId) -> bool) -> bool {
185181
true #(&& <#field_types>::matches_component_set(&state.#named_field_idents, _set_contains_id))*
186182
}

crates/bevy_ecs/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub mod prelude {
3636
component::Component,
3737
entity::Entity,
3838
event::{Event, EventReader, EventWriter, Events},
39-
query::{Added, AnyOf, Changed, Has, Or, QueryState, With, Without},
39+
query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without},
4040
removal_detection::RemovedComponents,
4141
schedule::{
4242
apply_deferred, apply_state_transition, common_conditions::*, Condition,

crates/bevy_ecs/src/query/access.rs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@ impl<T: SparseSetIndex> Access<T> {
157157
self.writes_all
158158
}
159159

160+
/// Removes all writes.
161+
pub fn clear_writes(&mut self) {
162+
self.writes_all = false;
163+
self.writes.clear();
164+
}
165+
160166
/// Removes all accesses.
161167
pub fn clear(&mut self) {
162168
self.reads_all = false;
@@ -198,6 +204,29 @@ impl<T: SparseSetIndex> Access<T> {
198204
&& other.writes.is_disjoint(&self.reads_and_writes)
199205
}
200206

207+
/// Returns `true` if the set is a subset of another, i.e. `other` contains
208+
/// at least all the values in `self`.
209+
pub fn is_subset(&self, other: &Access<T>) -> bool {
210+
if self.writes_all {
211+
return other.writes_all;
212+
}
213+
214+
if other.writes_all {
215+
return true;
216+
}
217+
218+
if self.reads_all {
219+
return other.reads_all;
220+
}
221+
222+
if other.reads_all {
223+
return self.writes.is_subset(&other.writes);
224+
}
225+
226+
self.reads_and_writes.is_subset(&other.reads_and_writes)
227+
&& self.writes.is_subset(&other.writes)
228+
}
229+
201230
/// Returns a vector of elements that the access and `other` cannot access at the same time.
202231
pub fn get_conflicts(&self, other: &Access<T>) -> Vec<T> {
203232
let mut conflicts = FixedBitSet::default();
@@ -267,16 +296,18 @@ impl<T: SparseSetIndex> Access<T> {
267296
/// See comments the [`WorldQuery`](super::WorldQuery) impls of [`AnyOf`](super::AnyOf)/`Option`/[`Or`](super::Or) for more information.
268297
#[derive(Debug, Clone, Eq, PartialEq)]
269298
pub struct FilteredAccess<T: SparseSetIndex> {
270-
access: Access<T>,
299+
pub(crate) access: Access<T>,
300+
pub(crate) required: FixedBitSet,
271301
// An array of filter sets to express `With` or `Without` clauses in disjunctive normal form, for example: `Or<(With<A>, With<B>)>`.
272302
// Filters like `(With<A>, Or<(With<B>, Without<C>)>` are expanded into `Or<((With<A>, With<B>), (With<A>, Without<C>))>`.
273-
filter_sets: Vec<AccessFilters<T>>,
303+
pub(crate) filter_sets: Vec<AccessFilters<T>>,
274304
}
275305

276306
impl<T: SparseSetIndex> Default for FilteredAccess<T> {
277307
fn default() -> Self {
278308
Self {
279309
access: Access::default(),
310+
required: FixedBitSet::default(),
280311
filter_sets: vec![AccessFilters::default()],
281312
}
282313
}
@@ -306,15 +337,23 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
306337
/// Adds access to the element given by `index`.
307338
pub fn add_read(&mut self, index: T) {
308339
self.access.add_read(index.clone());
340+
self.add_required(index.clone());
309341
self.and_with(index);
310342
}
311343

312344
/// Adds exclusive access to the element given by `index`.
313345
pub fn add_write(&mut self, index: T) {
314346
self.access.add_write(index.clone());
347+
self.add_required(index.clone());
315348
self.and_with(index);
316349
}
317350

351+
fn add_required(&mut self, index: T) {
352+
let index = index.sparse_set_index();
353+
self.required.grow(index + 1);
354+
self.required.insert(index);
355+
}
356+
318357
/// Adds a `With` filter: corresponds to a conjunction (AND) operation.
319358
///
320359
/// Suppose we begin with `Or<(With<A>, With<B>)>`, which is represented by an array of two `AccessFilter` instances.
@@ -391,6 +430,7 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
391430
/// `Or<((With<A>, With<C>), (With<A>, Without<D>), (Without<B>, With<C>), (Without<B>, Without<D>))>`.
392431
pub fn extend(&mut self, other: &FilteredAccess<T>) {
393432
self.access.extend(&other.access);
433+
self.required.union_with(&other.required);
394434

395435
// We can avoid allocating a new array of bitsets if `other` contains just a single set of filters:
396436
// in this case we can short-circuit by performing an in-place union for each bitset.
@@ -423,12 +463,18 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
423463
pub fn write_all(&mut self) {
424464
self.access.write_all();
425465
}
466+
467+
/// Returns `true` if the set is a subset of another, i.e. `other` contains
468+
/// at least all the values in `self`.
469+
pub fn is_subset(&self, other: &FilteredAccess<T>) -> bool {
470+
self.required.is_subset(&other.required) && self.access().is_subset(other.access())
471+
}
426472
}
427473

428474
#[derive(Clone, Eq, PartialEq)]
429-
struct AccessFilters<T> {
430-
with: FixedBitSet,
431-
without: FixedBitSet,
475+
pub(crate) struct AccessFilters<T> {
476+
pub(crate) with: FixedBitSet,
477+
pub(crate) without: FixedBitSet,
432478
_index_type: PhantomData<T>,
433479
}
434480

0 commit comments

Comments
 (0)