Skip to main content

bevy_extract/
sync_component.rs

1use core::marker::PhantomData;
2
3use bevy_app::{App, AppLabel, Plugin};
4use bevy_ecs::{
5    bundle::{Bundle, NoBundleEffect},
6    component::Component,
7    lifecycle::Remove,
8    observer::On,
9    system::ResMut,
10};
11
12use crate::sync_world::{EntityRecord, PendingSyncEntity, SyncToSubWorld};
13
14use bevy_log::warn_once;
15
16/// Plugin that registers a component for automatic sync to the sub world. See [`SyncWorldPlugin`] for more information.
17///
18/// This plugin is automatically added by [`ExtractComponentPlugin`], and only needs to be added for manual extraction implementations.
19///
20/// The marker type `F` is only used as a way to bypass the orphan rules. To
21/// implement the trait for a foreign type you can use a local type as the
22/// marker, e.g. the type of the plugin that calls [`SyncComponentPlugin`].
23///
24/// # Implementation details
25///
26/// It adds [`SyncToSubWorld`] as a required component to make the [`SyncWorldPlugin`] aware of the component, and
27/// handles cleanup of the component in the sub world when it is removed from an entity.
28///
29/// [`ExtractComponentPlugin`]: crate::extract_component::ExtractComponentPlugin
30/// [`SyncWorldPlugin`]: crate::sync_world::SyncWorldPlugin
31pub struct SyncComponentPlugin<C, L: AppLabel, F = ()>(PhantomData<(C, L, F)>);
32
33// pub type SyncComponentPlugin<C, F = ()> = SyncComponentPlugin<C, RenderApp, F>;
34
35impl<C: SyncComponent<L, F>, L: AppLabel, F> Default for SyncComponentPlugin<C, L, F> {
36    fn default() -> Self {
37        Self(PhantomData)
38    }
39}
40
41/// Trait that links components from the main world with output components in
42/// the sub world. It is used by [`SyncComponentPlugin`].
43///
44/// The marker type `F` is only used as a way to bypass the orphan rules. To
45/// implement the trait for a foreign type you can use a local type as the
46/// marker, e.g. the type of the plugin that calls [`SyncComponentPlugin`].
47///
48/// [`ExtractComponent`]: crate::extract_component::ExtractComponent
49pub trait SyncComponent<L: AppLabel, F = ()>: Component {
50    /// Describes what components should be removed from the sub world if the
51    /// implementing component is removed.
52    type Target: Bundle<Effect: NoBundleEffect>;
53    // TODO: https://github.com/rust-lang/rust/issues/29661
54    // type Target: Bundle<Effect: NoBundleEffect> = Self;
55}
56
57impl<
58        C: SyncComponent<L, F>,
59        L: AppLabel + Default + Clone + Copy + Eq,
60        F: Send + Sync + 'static,
61    > Plugin for SyncComponentPlugin<C, L, F>
62{
63    fn build(&self, app: &mut App) {
64        app.register_required_components::<C, SyncToSubWorld<L>>();
65
66        app.add_observer(
67            |remove: On<Remove<C>>, maybe_pending: Option<ResMut<PendingSyncEntity<L>>>| {
68                let Some(mut pending) = maybe_pending else {
69                    warn_once!("A component with sync plugin was removed, but the sub world does not exist, so there is nothing to sync. Skip sync to sub world.");
70                    return;
71                };
72                pending.push(EntityRecord::<L>::ComponentRemoved(
73                    remove.entity,
74                    |mut entity| {
75                        entity.remove::<C::Target>();
76                    },
77                ));
78            },
79        );
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use bevy_app::{App, AppLabel};
86    use bevy_ecs::component::Component;
87
88    use super::{SyncComponent, SyncComponentPlugin};
89
90    #[derive(AppLabel, Debug, Hash, PartialEq, Eq, Clone, Default, Copy)]
91    pub struct ExtractApp;
92
93    #[derive(Component)]
94    struct TestSyncComponent;
95
96    impl SyncComponent<ExtractApp> for TestSyncComponent {
97        type Target = Self;
98    }
99
100    // Regression test for https://github.com/bevyengine/bevy/issues/24927:
101    // with `WgpuSettings { backends: None }` there is no render world, and Bevy used to crash on removing any synced component.
102    // This test checks that the bug does not happen again.
103    #[test]
104    fn remove_synced_component_without_render_world() {
105        let mut app = App::new();
106        app.add_plugins(SyncComponentPlugin::<TestSyncComponent, ExtractApp>::default());
107
108        let entity = app.world_mut().spawn(TestSyncComponent).id();
109        app.world_mut().despawn(entity);
110    }
111}