bevy_app/
propagate.rs

1use alloc::vec::Vec;
2use core::marker::PhantomData;
3
4use crate::{App, Plugin};
5#[cfg(feature = "bevy_reflect")]
6use bevy_ecs::reflect::ReflectComponent;
7use bevy_ecs::{
8    component::Component,
9    entity::Entity,
10    hierarchy::ChildOf,
11    intern::Interned,
12    lifecycle::RemovedComponents,
13    query::{Changed, Or, QueryFilter, With, Without},
14    relationship::{Relationship, RelationshipTarget},
15    schedule::{IntoScheduleConfigs, ScheduleLabel, SystemSet},
16    system::{Commands, Local, Query},
17};
18#[cfg(feature = "bevy_reflect")]
19use bevy_reflect::Reflect;
20
21/// Plugin to automatically propagate a component value to all direct and transient relationship
22/// targets (e.g. [`bevy_ecs::hierarchy::Children`]) of entities with a [`Propagate`] component.
23///
24/// The plugin Will maintain the target component over hierarchy changes, adding or removing
25/// `C` when a relationship `R` (e.g. [`ChildOf`]) is added to or removed from a
26/// relationship tree with a [`Propagate<C>`] source, or if the [`Propagate<C>`] component
27/// is added, changed or removed.
28///
29/// Optionally you can include a query filter `F` to restrict the entities that are updated.
30/// Note that the filter is not rechecked dynamically: changes to the filter state will not be
31/// picked up until the  [`Propagate`] component is touched, or the hierarchy is changed.
32/// All members of the tree between source and target must match the filter for propagation
33/// to reach a given target.
34/// Individual entities can be skipped or terminate the propagation with the [`PropagateOver`]
35/// and [`PropagateStop`] components.
36///
37/// The schedule can be configured via [`HierarchyPropagatePlugin::new`].
38/// You should be sure to schedule your logic relative to this set: making changes
39/// that modify component values before this logic, and reading the propagated
40/// values after it.
41pub struct HierarchyPropagatePlugin<
42    C: Component + Clone + PartialEq,
43    F: QueryFilter = (),
44    R: Relationship = ChildOf,
45> {
46    schedule: Interned<dyn ScheduleLabel>,
47    _marker: PhantomData<fn() -> (C, F, R)>,
48}
49
50impl<C: Component + Clone + PartialEq, F: QueryFilter, R: Relationship>
51    HierarchyPropagatePlugin<C, F, R>
52{
53    /// Construct the plugin. The propagation systems will be placed in the specified schedule.
54    pub fn new(schedule: impl ScheduleLabel) -> Self {
55        Self {
56            schedule: schedule.intern(),
57            _marker: PhantomData,
58        }
59    }
60}
61
62/// Causes the inner component to be added to this entity and all direct and transient relationship
63/// targets. A target with a [`Propagate<C>`] component of its own will override propagation from
64/// that point in the tree.
65#[derive(Component, Clone, PartialEq)]
66#[cfg_attr(
67    feature = "bevy_reflect",
68    derive(Reflect),
69    reflect(Component, Clone, PartialEq)
70)]
71pub struct Propagate<C: Component + Clone + PartialEq>(pub C);
72
73/// Stops the output component being added to this entity.
74/// Relationship targets will still inherit the component from this entity or its parents.
75#[derive(Component)]
76#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))]
77pub struct PropagateOver<C>(PhantomData<fn() -> C>);
78
79/// Stops the propagation at this entity. Children will not inherit the component.
80#[derive(Component)]
81#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))]
82pub struct PropagateStop<C>(PhantomData<fn() -> C>);
83
84/// The set in which propagation systems are added. You can schedule your logic relative to this set.
85#[derive(SystemSet, Clone, PartialEq, PartialOrd, Ord)]
86pub struct PropagateSet<C: Component + Clone + PartialEq> {
87    _p: PhantomData<fn() -> C>,
88}
89
90/// Internal struct for managing propagation
91#[derive(Component, Clone, PartialEq)]
92#[cfg_attr(
93    feature = "bevy_reflect",
94    derive(Reflect),
95    reflect(Component, Clone, PartialEq)
96)]
97pub struct Inherited<C: Component + Clone + PartialEq>(pub C);
98
99impl<C> Default for PropagateOver<C> {
100    fn default() -> Self {
101        Self(Default::default())
102    }
103}
104
105impl<C> Default for PropagateStop<C> {
106    fn default() -> Self {
107        Self(Default::default())
108    }
109}
110
111impl<C: Component + Clone + PartialEq> core::fmt::Debug for PropagateSet<C> {
112    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
113        f.debug_struct("PropagateSet")
114            .field("_p", &self._p)
115            .finish()
116    }
117}
118
119impl<C: Component + Clone + PartialEq> Eq for PropagateSet<C> {}
120
121impl<C: Component + Clone + PartialEq> core::hash::Hash for PropagateSet<C> {
122    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
123        self._p.hash(state);
124    }
125}
126
127impl<C: Component + Clone + PartialEq> Default for PropagateSet<C> {
128    fn default() -> Self {
129        Self {
130            _p: Default::default(),
131        }
132    }
133}
134
135impl<C: Component + Clone + PartialEq, F: QueryFilter + 'static, R: Relationship> Plugin
136    for HierarchyPropagatePlugin<C, F, R>
137{
138    fn build(&self, app: &mut App) {
139        app.add_systems(
140            self.schedule,
141            (
142                update_source::<C, F>,
143                update_stopped::<C, F>,
144                update_reparented::<C, F, R>,
145                propagate_inherited::<C, F, R>,
146                propagate_output::<C, F>,
147            )
148                .chain()
149                .in_set(PropagateSet::<C>::default()),
150        );
151    }
152}
153
154/// add/remove `Inherited::<C>` and `C` for entities with a direct `Propagate::<C>`
155pub fn update_source<C: Component + Clone + PartialEq, F: QueryFilter>(
156    mut commands: Commands,
157    changed: Query<
158        (Entity, &Propagate<C>),
159        (
160            Or<(Changed<Propagate<C>>, Without<Inherited<C>>)>,
161            Without<PropagateStop<C>>,
162        ),
163    >,
164    mut removed: RemovedComponents<Propagate<C>>,
165) {
166    for (entity, source) in &changed {
167        commands
168            .entity(entity)
169            .try_insert(Inherited(source.0.clone()));
170    }
171
172    for removed in removed.read() {
173        if let Ok(mut commands) = commands.get_entity(removed) {
174            commands.remove::<(Inherited<C>, C)>();
175        }
176    }
177}
178
179/// remove `Inherited::<C>` and `C` for entities with a `PropagateStop::<C>`
180pub fn update_stopped<C: Component + Clone + PartialEq, F: QueryFilter>(
181    mut commands: Commands,
182    q: Query<Entity, (With<Inherited<C>>, With<PropagateStop<C>>, F)>,
183) {
184    for entity in q.iter() {
185        let mut cmds = commands.entity(entity);
186        cmds.remove::<(Inherited<C>, C)>();
187    }
188}
189
190/// add/remove `Inherited::<C>` and `C` for entities which have changed relationship
191pub fn update_reparented<C: Component + Clone + PartialEq, F: QueryFilter, R: Relationship>(
192    mut commands: Commands,
193    moved: Query<
194        (Entity, &R, Option<&Inherited<C>>),
195        (
196            Changed<R>,
197            Without<Propagate<C>>,
198            Without<PropagateStop<C>>,
199            F,
200        ),
201    >,
202    relations: Query<&Inherited<C>>,
203    orphaned: Query<Entity, (With<Inherited<C>>, Without<Propagate<C>>, Without<R>, F)>,
204) {
205    for (entity, relation, maybe_inherited) in &moved {
206        if let Ok(inherited) = relations.get(relation.get()) {
207            commands.entity(entity).try_insert(inherited.clone());
208        } else if maybe_inherited.is_some() {
209            commands.entity(entity).remove::<(Inherited<C>, C)>();
210        }
211    }
212
213    for orphan in &orphaned {
214        commands.entity(orphan).remove::<(Inherited<C>, C)>();
215    }
216}
217
218/// add/remove `Inherited::<C>` for targets of entities with modified `Inherited::<C>`
219pub fn propagate_inherited<C: Component + Clone + PartialEq, F: QueryFilter, R: Relationship>(
220    mut commands: Commands,
221    changed: Query<
222        (&Inherited<C>, &R::RelationshipTarget),
223        (Changed<Inherited<C>>, Without<PropagateStop<C>>, F),
224    >,
225    recurse: Query<
226        (Option<&R::RelationshipTarget>, Option<&Inherited<C>>),
227        (Without<Propagate<C>>, Without<PropagateStop<C>>, F),
228    >,
229    mut removed: RemovedComponents<Inherited<C>>,
230    mut to_process: Local<Vec<(Entity, Option<Inherited<C>>)>>,
231) {
232    // gather changed
233    for (inherited, targets) in &changed {
234        to_process.extend(
235            targets
236                .iter()
237                .map(|target| (target, Some(inherited.clone()))),
238        );
239    }
240
241    // and removed
242    for entity in removed.read() {
243        if let Ok((Some(targets), _)) = recurse.get(entity) {
244            to_process.extend(targets.iter().map(|target| (target, None)));
245        }
246    }
247
248    // propagate
249    while let Some((entity, maybe_inherited)) = (*to_process).pop() {
250        let Ok((maybe_targets, maybe_current)) = recurse.get(entity) else {
251            continue;
252        };
253
254        if maybe_current == maybe_inherited.as_ref() {
255            continue;
256        }
257
258        if let Some(targets) = maybe_targets {
259            to_process.extend(
260                targets
261                    .iter()
262                    .map(|target| (target, maybe_inherited.clone())),
263            );
264        }
265
266        if let Some(inherited) = maybe_inherited {
267            commands.entity(entity).try_insert(inherited.clone());
268        } else {
269            commands.entity(entity).remove::<(Inherited<C>, C)>();
270        }
271    }
272}
273
274/// add `C` to entities with `Inherited::<C>`
275pub fn propagate_output<C: Component + Clone + PartialEq, F: QueryFilter>(
276    mut commands: Commands,
277    changed: Query<
278        (Entity, &Inherited<C>, Option<&C>),
279        (Changed<Inherited<C>>, Without<PropagateOver<C>>, F),
280    >,
281) {
282    for (entity, inherited, maybe_current) in &changed {
283        if maybe_current.is_some_and(|c| &inherited.0 == c) {
284            continue;
285        }
286
287        commands.entity(entity).try_insert(inherited.0.clone());
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use bevy_ecs::schedule::Schedule;
294
295    use crate::{App, Update};
296
297    use super::*;
298
299    #[derive(Component, Clone, PartialEq, Debug)]
300    struct TestValue(u32);
301
302    #[test]
303    fn test_simple_propagate() {
304        let mut app = App::new();
305        app.add_schedule(Schedule::new(Update));
306        app.add_plugins(HierarchyPropagatePlugin::<TestValue>::new(Update));
307
308        let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
309        let intermediate = app
310            .world_mut()
311            .spawn_empty()
312            .insert(ChildOf(propagator))
313            .id();
314        let propagatee = app
315            .world_mut()
316            .spawn_empty()
317            .insert(ChildOf(intermediate))
318            .id();
319
320        app.update();
321
322        assert!(app
323            .world_mut()
324            .query::<&TestValue>()
325            .get(app.world(), propagatee)
326            .is_ok());
327    }
328
329    #[test]
330    fn test_reparented() {
331        let mut app = App::new();
332        app.add_schedule(Schedule::new(Update));
333        app.add_plugins(HierarchyPropagatePlugin::<TestValue>::new(Update));
334
335        let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
336        let propagatee = app
337            .world_mut()
338            .spawn_empty()
339            .insert(ChildOf(propagator))
340            .id();
341
342        app.update();
343
344        assert!(app
345            .world_mut()
346            .query::<&TestValue>()
347            .get(app.world(), propagatee)
348            .is_ok());
349    }
350
351    #[test]
352    fn test_reparented_with_prior() {
353        let mut app = App::new();
354        app.add_schedule(Schedule::new(Update));
355        app.add_plugins(HierarchyPropagatePlugin::<TestValue>::new(Update));
356
357        let propagator_a = app.world_mut().spawn(Propagate(TestValue(1))).id();
358        let propagator_b = app.world_mut().spawn(Propagate(TestValue(2))).id();
359        let propagatee = app
360            .world_mut()
361            .spawn_empty()
362            .insert(ChildOf(propagator_a))
363            .id();
364
365        app.update();
366        assert_eq!(
367            app.world_mut()
368                .query::<&TestValue>()
369                .get(app.world(), propagatee),
370            Ok(&TestValue(1))
371        );
372        app.world_mut()
373            .commands()
374            .entity(propagatee)
375            .insert(ChildOf(propagator_b));
376        app.update();
377        assert_eq!(
378            app.world_mut()
379                .query::<&TestValue>()
380                .get(app.world(), propagatee),
381            Ok(&TestValue(2))
382        );
383    }
384
385    #[test]
386    fn test_remove_orphan() {
387        let mut app = App::new();
388        app.add_schedule(Schedule::new(Update));
389        app.add_plugins(HierarchyPropagatePlugin::<TestValue>::new(Update));
390
391        let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
392        let propagatee = app
393            .world_mut()
394            .spawn_empty()
395            .insert(ChildOf(propagator))
396            .id();
397
398        app.update();
399        assert!(app
400            .world_mut()
401            .query::<&TestValue>()
402            .get(app.world(), propagatee)
403            .is_ok());
404        app.world_mut()
405            .commands()
406            .entity(propagatee)
407            .remove::<ChildOf>();
408        app.update();
409        assert!(app
410            .world_mut()
411            .query::<&TestValue>()
412            .get(app.world(), propagatee)
413            .is_err());
414    }
415
416    #[test]
417    fn test_remove_propagated() {
418        let mut app = App::new();
419        app.add_schedule(Schedule::new(Update));
420        app.add_plugins(HierarchyPropagatePlugin::<TestValue>::new(Update));
421
422        let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
423        let propagatee = app
424            .world_mut()
425            .spawn_empty()
426            .insert(ChildOf(propagator))
427            .id();
428
429        app.update();
430        assert!(app
431            .world_mut()
432            .query::<&TestValue>()
433            .get(app.world(), propagatee)
434            .is_ok());
435        app.world_mut()
436            .commands()
437            .entity(propagator)
438            .remove::<Propagate<TestValue>>();
439        app.update();
440        assert!(app
441            .world_mut()
442            .query::<&TestValue>()
443            .get(app.world(), propagatee)
444            .is_err());
445    }
446
447    #[test]
448    fn test_propagate_over() {
449        let mut app = App::new();
450        app.add_schedule(Schedule::new(Update));
451        app.add_plugins(HierarchyPropagatePlugin::<TestValue>::new(Update));
452
453        let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
454        let propagate_over = app
455            .world_mut()
456            .spawn(TestValue(2))
457            .insert(ChildOf(propagator))
458            .id();
459        let propagatee = app
460            .world_mut()
461            .spawn_empty()
462            .insert(ChildOf(propagate_over))
463            .id();
464
465        app.update();
466        assert_eq!(
467            app.world_mut()
468                .query::<&TestValue>()
469                .get(app.world(), propagatee),
470            Ok(&TestValue(1))
471        );
472    }
473
474    #[test]
475    fn test_propagate_stop() {
476        let mut app = App::new();
477        app.add_schedule(Schedule::new(Update));
478        app.add_plugins(HierarchyPropagatePlugin::<TestValue>::new(Update));
479
480        let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
481        let propagate_stop = app
482            .world_mut()
483            .spawn(PropagateStop::<TestValue>::default())
484            .insert(ChildOf(propagator))
485            .id();
486        let no_propagatee = app
487            .world_mut()
488            .spawn_empty()
489            .insert(ChildOf(propagate_stop))
490            .id();
491
492        app.update();
493        assert!(app
494            .world_mut()
495            .query::<&TestValue>()
496            .get(app.world(), no_propagatee)
497            .is_err());
498    }
499
500    #[test]
501    fn test_intermediate_override() {
502        let mut app = App::new();
503        app.add_schedule(Schedule::new(Update));
504        app.add_plugins(HierarchyPropagatePlugin::<TestValue>::new(Update));
505
506        let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
507        let intermediate = app
508            .world_mut()
509            .spawn_empty()
510            .insert(ChildOf(propagator))
511            .id();
512        let propagatee = app
513            .world_mut()
514            .spawn_empty()
515            .insert(ChildOf(intermediate))
516            .id();
517
518        app.update();
519        assert_eq!(
520            app.world_mut()
521                .query::<&TestValue>()
522                .get(app.world(), propagatee),
523            Ok(&TestValue(1))
524        );
525
526        app.world_mut()
527            .entity_mut(intermediate)
528            .insert(Propagate(TestValue(2)));
529        app.update();
530        assert_eq!(
531            app.world_mut()
532                .query::<&TestValue>()
533                .get(app.world(), propagatee),
534            Ok(&TestValue(2))
535        );
536    }
537
538    #[test]
539    fn test_filter() {
540        #[derive(Component)]
541        struct Marker;
542
543        let mut app = App::new();
544        app.add_schedule(Schedule::new(Update));
545        app.add_plugins(HierarchyPropagatePlugin::<TestValue, With<Marker>>::new(
546            Update,
547        ));
548
549        let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
550        let propagatee = app
551            .world_mut()
552            .spawn_empty()
553            .insert(ChildOf(propagator))
554            .id();
555
556        app.update();
557        assert!(app
558            .world_mut()
559            .query::<&TestValue>()
560            .get(app.world(), propagatee)
561            .is_err());
562
563        // NOTE: changes to the filter condition are not rechecked
564        app.world_mut().entity_mut(propagator).insert(Marker);
565        app.world_mut().entity_mut(propagatee).insert(Marker);
566        app.update();
567        assert!(app
568            .world_mut()
569            .query::<&TestValue>()
570            .get(app.world(), propagatee)
571            .is_err());
572
573        app.world_mut()
574            .entity_mut(propagator)
575            .insert(Propagate(TestValue(1)));
576        app.update();
577        assert!(app
578            .world_mut()
579            .query::<&TestValue>()
580            .get(app.world(), propagatee)
581            .is_ok());
582    }
583}