use alloc::vec::Vec; use core::marker::PhantomData; use crate::{App, Plugin, Update}; use bevy_ecs::{ component::Component, entity::Entity, hierarchy::ChildOf, lifecycle::RemovedComponents, query::{Changed, Or, QueryFilter, With, Without}, relationship::{Relationship, RelationshipTarget}, schedule::{IntoScheduleConfigs, SystemSet}, system::{Commands, Local, Query}, }; /// Plugin to automatically propagate a component value to all direct and transient relationship /// targets (e.g. [`bevy_ecs::hierarchy::Children`]) of entities with a [`Propagate`] component. /// /// The plugin Will maintain the target component over hierarchy changes, adding or removing /// `C` when a relationship `R` (e.g. [`ChildOf`]) is added to or removed from a /// relationship tree with a [`Propagate`] source, or if the [`Propagate`] component /// is added, changed or removed. /// /// Optionally you can include a query filter `F` to restrict the entities that are updated. /// Note that the filter is not rechecked dynamically: changes to the filter state will not be /// picked up until the [`Propagate`] component is touched, or the hierarchy is changed. /// All members of the tree between source and target must match the filter for propagation /// to reach a given target. /// Individual entities can be skipped or terminate the propagation with the [`PropagateOver`] /// and [`PropagateStop`] components. pub struct HierarchyPropagatePlugin< C: Component + Clone + PartialEq, F: QueryFilter = (), R: Relationship = ChildOf, >(PhantomData (C, F, R)>); /// Causes the inner component to be added to this entity and all direct and transient relationship /// targets. A target with a [`Propagate`] component of its own will override propagation from /// that point in the tree. #[derive(Component, Clone, PartialEq)] pub struct Propagate(pub C); /// Stops the output component being added to this entity. /// Relationship targets will still inherit the component from this entity or its parents. #[derive(Component)] pub struct PropagateOver(PhantomData C>); /// Stops the propagation at this entity. Children will not inherit the component. #[derive(Component)] pub struct PropagateStop(PhantomData C>); /// The set in which propagation systems are added. You can schedule your logic relative to this set. #[derive(SystemSet, Clone, PartialEq, PartialOrd, Ord)] pub struct PropagateSet { _p: PhantomData C>, } /// Internal struct for managing propagation #[derive(Component, Clone, PartialEq)] pub struct Inherited(pub C); impl Default for HierarchyPropagatePlugin { fn default() -> Self { Self(Default::default()) } } impl Default for PropagateOver { fn default() -> Self { Self(Default::default()) } } impl Default for PropagateStop { fn default() -> Self { Self(Default::default()) } } impl core::fmt::Debug for PropagateSet { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("PropagateSet") .field("_p", &self._p) .finish() } } impl Eq for PropagateSet {} impl core::hash::Hash for PropagateSet { fn hash(&self, state: &mut H) { self._p.hash(state); } } impl Default for PropagateSet { fn default() -> Self { Self { _p: Default::default(), } } } impl Plugin for HierarchyPropagatePlugin { fn build(&self, app: &mut App) { app.add_systems( Update, ( update_source::, update_stopped::, update_reparented::, propagate_inherited::, propagate_output::, ) .chain() .in_set(PropagateSet::::default()), ); } } /// add/remove `Inherited::` and `C` for entities with a direct `Propagate::` pub fn update_source( mut commands: Commands, changed: Query< (Entity, &Propagate), ( Or<(Changed>, Without>)>, Without>, ), >, mut removed: RemovedComponents>, ) { for (entity, source) in &changed { commands .entity(entity) .try_insert(Inherited(source.0.clone())); } for removed in removed.read() { if let Ok(mut commands) = commands.get_entity(removed) { commands.remove::<(Inherited, C)>(); } } } /// remove `Inherited::` and `C` for entities with a `PropagateStop::` pub fn update_stopped( mut commands: Commands, q: Query>, With>, F)>, ) { for entity in q.iter() { let mut cmds = commands.entity(entity); cmds.remove::<(Inherited, C)>(); } } /// add/remove `Inherited::` and `C` for entities which have changed relationship pub fn update_reparented( mut commands: Commands, moved: Query< (Entity, &R, Option<&Inherited>), ( Changed, Without>, Without>, F, ), >, relations: Query<&Inherited>, orphaned: Query>, Without>, Without, F)>, ) { for (entity, relation, maybe_inherited) in &moved { if let Ok(inherited) = relations.get(relation.get()) { commands.entity(entity).try_insert(inherited.clone()); } else if maybe_inherited.is_some() { commands.entity(entity).remove::<(Inherited, C)>(); } } for orphan in &orphaned { commands.entity(orphan).remove::<(Inherited, C)>(); } } /// add/remove `Inherited::` for targets of entities with modified `Inherited::` pub fn propagate_inherited( mut commands: Commands, changed: Query< (&Inherited, &R::RelationshipTarget), (Changed>, Without>, F), >, recurse: Query< (Option<&R::RelationshipTarget>, Option<&Inherited>), (Without>, Without>, F), >, mut removed: RemovedComponents>, mut to_process: Local>)>>, ) { // gather changed for (inherited, targets) in &changed { to_process.extend( targets .iter() .map(|target| (target, Some(inherited.clone()))), ); } // and removed for entity in removed.read() { if let Ok((Some(targets), _)) = recurse.get(entity) { to_process.extend(targets.iter().map(|target| (target, None))); } } // propagate while let Some((entity, maybe_inherited)) = (*to_process).pop() { let Ok((maybe_targets, maybe_current)) = recurse.get(entity) else { continue; }; if maybe_current == maybe_inherited.as_ref() { continue; } if let Some(targets) = maybe_targets { to_process.extend( targets .iter() .map(|target| (target, maybe_inherited.clone())), ); } if let Some(inherited) = maybe_inherited { commands.entity(entity).try_insert(inherited.clone()); } else { commands.entity(entity).remove::<(Inherited, C)>(); } } } /// add `C` to entities with `Inherited::` pub fn propagate_output( mut commands: Commands, changed: Query< (Entity, &Inherited, Option<&C>), (Changed>, Without>, F), >, ) { for (entity, inherited, maybe_current) in &changed { if maybe_current.is_some_and(|c| &inherited.0 == c) { continue; } commands.entity(entity).try_insert(inherited.0.clone()); } } #[cfg(test)] mod tests { use bevy_ecs::schedule::Schedule; use crate::{App, Update}; use super::*; #[derive(Component, Clone, PartialEq, Debug)] struct TestValue(u32); #[test] fn test_simple_propagate() { let mut app = App::new(); app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::::default()); let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); let intermediate = app .world_mut() .spawn_empty() .insert(ChildOf(propagator)) .id(); let propagatee = app .world_mut() .spawn_empty() .insert(ChildOf(intermediate)) .id(); app.update(); assert!(app .world_mut() .query::<&TestValue>() .get(app.world(), propagatee) .is_ok()); } #[test] fn test_reparented() { let mut app = App::new(); app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::::default()); let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); let propagatee = app .world_mut() .spawn_empty() .insert(ChildOf(propagator)) .id(); app.update(); assert!(app .world_mut() .query::<&TestValue>() .get(app.world(), propagatee) .is_ok()); } #[test] fn test_reparented_with_prior() { let mut app = App::new(); app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::::default()); let propagator_a = app.world_mut().spawn(Propagate(TestValue(1))).id(); let propagator_b = app.world_mut().spawn(Propagate(TestValue(2))).id(); let propagatee = app .world_mut() .spawn_empty() .insert(ChildOf(propagator_a)) .id(); app.update(); assert_eq!( app.world_mut() .query::<&TestValue>() .get(app.world(), propagatee), Ok(&TestValue(1)) ); app.world_mut() .commands() .entity(propagatee) .insert(ChildOf(propagator_b)); app.update(); assert_eq!( app.world_mut() .query::<&TestValue>() .get(app.world(), propagatee), Ok(&TestValue(2)) ); } #[test] fn test_remove_orphan() { let mut app = App::new(); app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::::default()); let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); let propagatee = app .world_mut() .spawn_empty() .insert(ChildOf(propagator)) .id(); app.update(); assert!(app .world_mut() .query::<&TestValue>() .get(app.world(), propagatee) .is_ok()); app.world_mut() .commands() .entity(propagatee) .remove::(); app.update(); assert!(app .world_mut() .query::<&TestValue>() .get(app.world(), propagatee) .is_err()); } #[test] fn test_remove_propagated() { let mut app = App::new(); app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::::default()); let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); let propagatee = app .world_mut() .spawn_empty() .insert(ChildOf(propagator)) .id(); app.update(); assert!(app .world_mut() .query::<&TestValue>() .get(app.world(), propagatee) .is_ok()); app.world_mut() .commands() .entity(propagator) .remove::>(); app.update(); assert!(app .world_mut() .query::<&TestValue>() .get(app.world(), propagatee) .is_err()); } #[test] fn test_propagate_over() { let mut app = App::new(); app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::::default()); let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); let propagate_over = app .world_mut() .spawn(TestValue(2)) .insert(ChildOf(propagator)) .id(); let propagatee = app .world_mut() .spawn_empty() .insert(ChildOf(propagate_over)) .id(); app.update(); assert_eq!( app.world_mut() .query::<&TestValue>() .get(app.world(), propagatee), Ok(&TestValue(1)) ); } #[test] fn test_propagate_stop() { let mut app = App::new(); app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::::default()); let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); let propagate_stop = app .world_mut() .spawn(PropagateStop::::default()) .insert(ChildOf(propagator)) .id(); let no_propagatee = app .world_mut() .spawn_empty() .insert(ChildOf(propagate_stop)) .id(); app.update(); assert!(app .world_mut() .query::<&TestValue>() .get(app.world(), no_propagatee) .is_err()); } #[test] fn test_intermediate_override() { let mut app = App::new(); app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::::default()); let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); let intermediate = app .world_mut() .spawn_empty() .insert(ChildOf(propagator)) .id(); let propagatee = app .world_mut() .spawn_empty() .insert(ChildOf(intermediate)) .id(); app.update(); assert_eq!( app.world_mut() .query::<&TestValue>() .get(app.world(), propagatee), Ok(&TestValue(1)) ); app.world_mut() .entity_mut(intermediate) .insert(Propagate(TestValue(2))); app.update(); assert_eq!( app.world_mut() .query::<&TestValue>() .get(app.world(), propagatee), Ok(&TestValue(2)) ); } #[test] fn test_filter() { #[derive(Component)] struct Marker; let mut app = App::new(); app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::>::default()); let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); let propagatee = app .world_mut() .spawn_empty() .insert(ChildOf(propagator)) .id(); app.update(); assert!(app .world_mut() .query::<&TestValue>() .get(app.world(), propagatee) .is_err()); // NOTE: changes to the filter condition are not rechecked app.world_mut().entity_mut(propagator).insert(Marker); app.world_mut().entity_mut(propagatee).insert(Marker); app.update(); assert!(app .world_mut() .query::<&TestValue>() .get(app.world(), propagatee) .is_err()); app.world_mut() .entity_mut(propagator) .insert(Propagate(TestValue(1))); app.update(); assert!(app .world_mut() .query::<&TestValue>() .get(app.world(), propagatee) .is_ok()); } }