Skip to content

Commit f97eba2

Browse files
authored
Add VisitEntities for generic and reflectable Entity iteration (#15425)
# Objective - Provide a generic and _reflectable_ way to iterate over contained entities ## Solution Adds two new traits: * `VisitEntities`: Reflectable iteration, accepts a closure rather than producing an iterator. Implemented by default for `IntoIterator` implementing types. A proc macro is also provided. * A `Mut` variant of the above. Its derive macro uses the same field attribute to avoid repetition. ## Testing Added a test for `VisitEntities` that also transitively tests its derive macro as well as the default `MapEntities` impl.
1 parent 40c26f8 commit f97eba2

File tree

14 files changed

+426
-64
lines changed

14 files changed

+426
-64
lines changed

crates/bevy_animation/src/lib.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ use bevy_app::{App, Plugin, PostUpdate};
2828
use bevy_asset::{Asset, AssetApp, Assets, Handle};
2929
use bevy_core::Name;
3030
use bevy_ecs::{
31-
entity::MapEntities, prelude::*, reflect::ReflectMapEntities, world::EntityMutExcept,
31+
entity::{VisitEntities, VisitEntitiesMut},
32+
prelude::*,
33+
reflect::{ReflectMapEntities, ReflectVisitEntities, ReflectVisitEntitiesMut},
34+
world::EntityMutExcept,
3235
};
3336
use bevy_math::FloatExt;
3437
use bevy_reflect::{
@@ -527,12 +530,13 @@ impl Hash for AnimationTargetId {
527530
/// Note that each entity can only be animated by one animation player at a
528531
/// time. However, you can change [`AnimationTarget`]'s `player` property at
529532
/// runtime to change which player is responsible for animating the entity.
530-
#[derive(Clone, Copy, Component, Reflect)]
531-
#[reflect(Component, MapEntities)]
533+
#[derive(Clone, Copy, Component, Reflect, VisitEntities, VisitEntitiesMut)]
534+
#[reflect(Component, MapEntities, VisitEntities, VisitEntitiesMut)]
532535
pub struct AnimationTarget {
533536
/// The ID of this animation target.
534537
///
535538
/// Typically, this is derived from the path.
539+
#[visit_entities(ignore)]
536540
pub id: AnimationTargetId,
537541

538542
/// The entity containing the [`AnimationPlayer`].
@@ -1298,12 +1302,6 @@ impl From<&Name> for AnimationTargetId {
12981302
}
12991303
}
13001304

1301-
impl MapEntities for AnimationTarget {
1302-
fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
1303-
self.player = entity_mapper.map_entity(self.player);
1304-
}
1305-
}
1306-
13071305
impl AnimationGraphEvaluator {
13081306
// Starts a new depth-first search.
13091307
fn reset(&mut self, root: AnimationNodeIndex, node_count: usize) {

crates/bevy_ecs/macros/src/lib.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::{query_data::derive_query_data_impl, query_filter::derive_query_filte
1414
use bevy_macro_utils::{derive_label, ensure_no_collision, get_struct_fields, BevyManifest};
1515
use proc_macro::TokenStream;
1616
use proc_macro2::Span;
17+
use proc_macro2::TokenStream as TokenStream2;
1718
use quote::{format_ident, quote};
1819
use syn::{
1920
parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma,
@@ -180,6 +181,110 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
180181
})
181182
}
182183

184+
fn derive_visit_entities_base(
185+
input: TokenStream,
186+
trait_name: TokenStream2,
187+
gen_methods: impl FnOnce(Vec<TokenStream2>) -> TokenStream2,
188+
) -> TokenStream {
189+
let ast = parse_macro_input!(input as DeriveInput);
190+
let ecs_path = bevy_ecs_path();
191+
192+
let named_fields = match get_struct_fields(&ast.data) {
193+
Ok(fields) => fields,
194+
Err(e) => return e.into_compile_error().into(),
195+
};
196+
197+
let field = named_fields
198+
.iter()
199+
.filter_map(|field| {
200+
if let Some(attr) = field
201+
.attrs
202+
.iter()
203+
.find(|a| a.path().is_ident("visit_entities"))
204+
{
205+
let ignore = attr.parse_nested_meta(|meta| {
206+
if meta.path.is_ident("ignore") {
207+
Ok(())
208+
} else {
209+
Err(meta.error("Invalid visit_entities attribute. Use `ignore`"))
210+
}
211+
});
212+
return match ignore {
213+
Ok(()) => None,
214+
Err(e) => Some(Err(e)),
215+
};
216+
}
217+
Some(Ok(field))
218+
})
219+
.map(|res| res.map(|field| field.ident.as_ref()))
220+
.collect::<Result<Vec<_>, _>>();
221+
222+
let field = match field {
223+
Ok(field) => field,
224+
Err(e) => return e.into_compile_error().into(),
225+
};
226+
227+
if field.is_empty() {
228+
return syn::Error::new(
229+
ast.span(),
230+
format!("Invalid `{}` type: at least one field", trait_name),
231+
)
232+
.into_compile_error()
233+
.into();
234+
}
235+
236+
let field_access = field
237+
.iter()
238+
.enumerate()
239+
.map(|(n, f)| {
240+
if let Some(ident) = f {
241+
quote! {
242+
self.#ident
243+
}
244+
} else {
245+
let idx = Index::from(n);
246+
quote! {
247+
self.#idx
248+
}
249+
}
250+
})
251+
.collect::<Vec<_>>();
252+
253+
let methods = gen_methods(field_access);
254+
255+
let generics = ast.generics;
256+
let (impl_generics, ty_generics, _) = generics.split_for_impl();
257+
let struct_name = &ast.ident;
258+
259+
TokenStream::from(quote! {
260+
impl #impl_generics #ecs_path::entity:: #trait_name for #struct_name #ty_generics {
261+
#methods
262+
}
263+
})
264+
}
265+
266+
#[proc_macro_derive(VisitEntitiesMut, attributes(visit_entities))]
267+
pub fn derive_visit_entities_mut(input: TokenStream) -> TokenStream {
268+
derive_visit_entities_base(input, quote! { VisitEntitiesMut }, |field| {
269+
quote! {
270+
fn visit_entities_mut<F: FnMut(&mut Entity)>(&mut self, mut f: F) {
271+
#(#field.visit_entities_mut(&mut f);)*
272+
}
273+
}
274+
})
275+
}
276+
277+
#[proc_macro_derive(VisitEntities, attributes(visit_entities))]
278+
pub fn derive_visit_entities(input: TokenStream) -> TokenStream {
279+
derive_visit_entities_base(input, quote! { VisitEntities }, |field| {
280+
quote! {
281+
fn visit_entities<F: FnMut(Entity)>(&self, mut f: F) {
282+
#(#field.visit_entities(&mut f);)*
283+
}
284+
}
285+
})
286+
}
287+
183288
fn get_idents(fmt_string: fn(usize) -> String, count: usize) -> Vec<Ident> {
184289
(0..count)
185290
.map(|i| Ident::new(&fmt_string(i), Span::call_site()))

crates/bevy_ecs/src/entity/map_entities.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::{
44
world::World,
55
};
66

7-
use super::EntityHashMap;
7+
use super::{EntityHashMap, VisitEntitiesMut};
88

99
/// Operation to map all contained [`Entity`] fields in a type to new values.
1010
///
@@ -45,6 +45,14 @@ pub trait MapEntities {
4545
fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M);
4646
}
4747

48+
impl<T: VisitEntitiesMut> MapEntities for T {
49+
fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
50+
self.visit_entities_mut(|entity| {
51+
*entity = entity_mapper.map_entity(*entity);
52+
});
53+
}
54+
}
55+
4856
/// An implementor of this trait knows how to map an [`Entity`] into another [`Entity`].
4957
///
5058
/// Usually this is done by using an [`EntityHashMap<Entity>`] to map source entities
@@ -122,6 +130,16 @@ impl<T: EntityMapper> DynEntityMapper for T {
122130
}
123131
}
124132

133+
impl<'a> EntityMapper for &'a mut dyn DynEntityMapper {
134+
fn map_entity(&mut self, entity: Entity) -> Entity {
135+
(*self).dyn_map_entity(entity)
136+
}
137+
138+
fn mappings(&self) -> impl Iterator<Item = (Entity, Entity)> {
139+
(*self).dyn_mappings().into_iter()
140+
}
141+
}
142+
125143
impl EntityMapper for SceneEntityMapper<'_> {
126144
/// Returns the corresponding mapped entity or reserves a new dead entity ID in the current world if it is absent.
127145
fn map_entity(&mut self, entity: Entity) -> Entity {
@@ -152,8 +170,7 @@ impl EntityMapper for SceneEntityMapper<'_> {
152170
/// world. These newly allocated references are guaranteed to never point to any living entity in that world.
153171
///
154172
/// References are allocated by returning increasing generations starting from an internally initialized base
155-
/// [`Entity`]. After it is finished being used by [`MapEntities`] implementations, this entity is despawned and the
156-
/// requisite number of generations reserved.
173+
/// [`Entity`]. After it is finished being used, this entity is despawned and the requisite number of generations reserved.
157174
pub struct SceneEntityMapper<'m> {
158175
/// A mapping from one set of entities to another.
159176
///

crates/bevy_ecs/src/entity/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@
3636
//! [`EntityWorldMut::insert`]: crate::world::EntityWorldMut::insert
3737
//! [`EntityWorldMut::remove`]: crate::world::EntityWorldMut::remove
3838
mod map_entities;
39+
mod visit_entities;
3940
#[cfg(feature = "bevy_reflect")]
4041
use bevy_reflect::Reflect;
4142
#[cfg(all(feature = "bevy_reflect", feature = "serialize"))]
4243
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
4344
pub use map_entities::*;
45+
pub use visit_entities::*;
4446

4547
mod hash;
4648
pub use hash::*;
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
pub use bevy_ecs_macros::{VisitEntities, VisitEntitiesMut};
2+
3+
use crate::entity::Entity;
4+
5+
/// Apply an operation to all entities in a container.
6+
///
7+
/// This is implemented by default for types that implement [`IntoIterator`].
8+
///
9+
/// It may be useful to implement directly for types that can't produce an
10+
/// iterator for lifetime reasons, such as those involving internal mutexes.
11+
pub trait VisitEntities {
12+
/// Apply an operation to all contained entities.
13+
fn visit_entities<F: FnMut(Entity)>(&self, f: F);
14+
}
15+
16+
impl<T> VisitEntities for T
17+
where
18+
for<'a> &'a T: IntoIterator<Item = &'a Entity>,
19+
{
20+
fn visit_entities<F: FnMut(Entity)>(&self, f: F) {
21+
self.into_iter().copied().for_each(f);
22+
}
23+
}
24+
25+
impl VisitEntities for Entity {
26+
fn visit_entities<F: FnMut(Entity)>(&self, mut f: F) {
27+
f(*self);
28+
}
29+
}
30+
31+
/// Apply an operation to mutable references to all entities in a container.
32+
///
33+
/// This is implemented by default for types that implement [`IntoIterator`].
34+
///
35+
/// It may be useful to implement directly for types that can't produce an
36+
/// iterator for lifetime reasons, such as those involving internal mutexes.
37+
pub trait VisitEntitiesMut: VisitEntities {
38+
/// Apply an operation to mutable references to all contained entities.
39+
fn visit_entities_mut<F: FnMut(&mut Entity)>(&mut self, f: F);
40+
}
41+
42+
impl<T: VisitEntities> VisitEntitiesMut for T
43+
where
44+
for<'a> &'a mut T: IntoIterator<Item = &'a mut Entity>,
45+
{
46+
fn visit_entities_mut<F: FnMut(&mut Entity)>(&mut self, f: F) {
47+
self.into_iter().for_each(f);
48+
}
49+
}
50+
51+
impl VisitEntitiesMut for Entity {
52+
fn visit_entities_mut<F: FnMut(&mut Entity)>(&mut self, mut f: F) {
53+
f(self);
54+
}
55+
}
56+
57+
#[cfg(test)]
58+
mod tests {
59+
use crate::{
60+
self as bevy_ecs,
61+
entity::{EntityHashMap, MapEntities, SceneEntityMapper},
62+
world::World,
63+
};
64+
use bevy_utils::HashSet;
65+
66+
use super::*;
67+
68+
#[derive(VisitEntities, Debug, PartialEq)]
69+
struct Foo {
70+
ordered: Vec<Entity>,
71+
unordered: HashSet<Entity>,
72+
single: Entity,
73+
#[allow(dead_code)]
74+
#[visit_entities(ignore)]
75+
not_an_entity: String,
76+
}
77+
78+
// Need a manual impl since VisitEntitiesMut isn't implemented for `HashSet`.
79+
// We don't expect users to actually do this - it's only for test purposes
80+
// to prove out the automatic `MapEntities` impl we get with `VisitEntitiesMut`.
81+
impl VisitEntitiesMut for Foo {
82+
fn visit_entities_mut<F: FnMut(&mut Entity)>(&mut self, mut f: F) {
83+
self.ordered.visit_entities_mut(&mut f);
84+
self.unordered = self
85+
.unordered
86+
.drain()
87+
.map(|mut entity| {
88+
f(&mut entity);
89+
entity
90+
})
91+
.collect();
92+
f(&mut self.single);
93+
}
94+
}
95+
96+
#[test]
97+
fn visit_entities() {
98+
let mut world = World::new();
99+
let entities = world.entities();
100+
let mut foo = Foo {
101+
ordered: vec![entities.reserve_entity(), entities.reserve_entity()],
102+
unordered: [
103+
entities.reserve_entity(),
104+
entities.reserve_entity(),
105+
entities.reserve_entity(),
106+
]
107+
.into_iter()
108+
.collect(),
109+
single: entities.reserve_entity(),
110+
not_an_entity: "Bar".into(),
111+
};
112+
113+
let mut entity_map = EntityHashMap::<Entity>::default();
114+
let mut remapped = Foo {
115+
ordered: vec![],
116+
unordered: HashSet::new(),
117+
single: Entity::PLACEHOLDER,
118+
not_an_entity: foo.not_an_entity.clone(),
119+
};
120+
121+
// Note: this assumes that the VisitEntities derive is field-ordered,
122+
// which isn't explicitly stated/guaranteed.
123+
// If that changes, this test will fail, but that might be OK if
124+
// we're intentionally breaking that assumption.
125+
let mut i = 0;
126+
foo.visit_entities(|entity| {
127+
let new_entity = entities.reserve_entity();
128+
if i < foo.ordered.len() {
129+
assert_eq!(entity, foo.ordered[i]);
130+
remapped.ordered.push(new_entity);
131+
} else if i < foo.ordered.len() + foo.unordered.len() {
132+
assert!(foo.unordered.contains(&entity));
133+
remapped.unordered.insert(new_entity);
134+
} else {
135+
assert_eq!(entity, foo.single);
136+
remapped.single = new_entity;
137+
}
138+
139+
entity_map.insert(entity, new_entity);
140+
141+
i += 1;
142+
});
143+
144+
SceneEntityMapper::world_scope(&mut entity_map, &mut world, |_, mapper| {
145+
foo.map_entities(mapper);
146+
});
147+
148+
assert_eq!(foo, remapped);
149+
}
150+
}

0 commit comments

Comments
 (0)