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}