bevy
bevy copied to clipboard
Add methods for joining queries with lists.
Objective
This is similar in nature to #4879.
Add new new methods iter_join_map(_mut)
to reduce the need to use Query::get
.
These methods, compared to Query::get
:
- Have reduced overhead.
- Do not require a match; Reduced rightward drift.
- Improved ergonomics (for the immutable variants, at least).
I also took the liberty to rename iter_many
to iter_list
, plus some other changes listed further below.
The name iter_join_map
was chosen as the operation resembles an inner join on Entity
between a Query
and an arbitrary list that is mapped to Entity
via the supplied fn.
Explaining is hard. Do take a look at the examples.
Examples
#[derive(Component)]
struct Health {
value: f32
}
#[derive(Component)]
struct DamageEvent {
target: Entity,
damage: f32,
}
fn system(
mut damage_events: EventReader<DamageEvent>,
health_query: Query<&Health>,
) {
for (health, event) in
health_query.iter_join_map(damage_events.iter(), |event| event.target)
{
println!("Entity has {} health and will take {} damage!", health.value, event.damage);
}
}
fn system(
mut damage_events: EventReader<DamageEvent>,
mut health_query: Query<&mut Health>,
) {
let mut join = health_query.iter_join_map_mut(damage_events.iter(), |event| event.target);
while let Some((mut health, event)) = join.fetch_next() {
health.value -= event.damage;
}
}
The same systems without this PR.
fn system(
child_query: Query<(&Parent, &Health)>,
parent_query: Query<&Health>,
) {
for event in damage_events.iter() {
if let Ok(health) = health_query.get(event.target) {
println!("Entity has {} health and will take {} damage!", health.value, event.damage);
}
}
}
fn system(
mut damage_events: EventReader<DamageEvent>,
mut health_query: Query<&mut Health>,
) {
for event in damage_events.iter() {
if let Ok(mut health) = health_query.get_mut(event.target) {
health.value -= event.damage;
}
}
}
More examples.
fn system(
child_query: Query<(&Parent, &Health)>,
parent_query: Query<&Health>,
) {
for (parent_health, (_, health)) in
parent_query.iter_join_map(&child_query, |(parent, _)| parent.0)
{
println!(
"This entity's health is {}, and its parent's health is {}.",
health.0, parent_health.0
);
}
}
fn system(parent_query: Query<(&Children, &Health)>, child_query: Query<&Health>) {
let parent_iter = parent_query
.iter()
.flat_map(|(children, health)| children.iter().map(move |c| (*c, health)));
for (child_health, (_, parent_health)) in
child_query.iter_join_map(parent_iter, |(parent, _)| *parent)
{
println!(
"This child entity's health is {}, and its parent's health is {}.",
child_health.0, parent_health.0
);
}
}
If we had lending iterators :'( the mutable variant would simply look like this:
for (mut health, event) in
health_query.iter_join_map_mut(damage_events.iter(), |event| event.target)
{
health.value -= event.damage;
}
Changes
I tried seperating this into a couple commits to make it easier to review.
- https://github.com/bevyengine/bevy/commit/d4b825edf2a4e8734321bb4bbd33c029a5d7f7ed Add
iter_join_map
anditer_join_map_mut
toQuery
. - https://github.com/bevyengine/bevy/commit/65494091b53f22009fd90bf27a09ec9f562ab908 Remove
many_for_each_mut
, renameiter_many
toiter_list
and additer_list_mut
. - https://github.com/bevyengine/bevy/commit/7b08d5f93d0daa8c496765fae8f87ac8ff551ca1 Fix soundness regression on
main
foriter_combinations
. It was capable of returning mutable data. - https://github.com/bevyengine/bevy/commit/5b3dfdcc032bc34d2b5ed266ec2a01115ff3128f
EventWriter::send_batch
now takesIntoIterator
instead of justIterator
. - Moved tests to a more appropriate spot.
Instead of a for_each_mut
variant I decide to re-use the same tricks iter_combinations
uses to avoid aliased mutability.
iter_list
was updated to use this method as well.
Some Questions
You may have noticed the QueryJoinMapIter
and QueryEntityListIter
are practically identical.
I tried to re-use QueryJoinMapIter
with a hard-coded closure for map_f
for iter_list
, but in the return type it needs a concrete type for MapFn
.
It seems impossible to get a concrete type from a function, or at least I don't know how.
I added #[inline(always)]
to the new iterator next
methods, mimicking the other iterators.
Is that the right call?
Follow up
- It would be easy to add a right join as well. Outer join and left join will be trickier.
- More tests to prevent this kind of soundness regression.
- Add benchmarks for
iter_list
anditer_join_map
.
Does this fully solve #1658?
Does this fully solve #1658?
Not quite, #1658 mentions a join of two queries that produces a new Query
which I do think is possible, but not for me haha.
I do think this PR makes iterating over 'relations' nicer, so it improves the situation a bit.
Converting to draft because I'd like to base this on #5170 which will fix the unsoundness instead.
I'll rework this and split out the changes to iter_many
.