From 8243b57f3cab2a76e9dde8ce8b0319c10e73dfea Mon Sep 17 00:00:00 2001 From: andriyDev Date: Thu, 22 Jan 2026 00:12:46 -0800 Subject: [PATCH 1/4] Delete validate_parent_has_component and replace it with a whole plugin. --- crates/bevy_app/src/hierarchy.rs | 137 +++++++++++++++++++++++++++++++ crates/bevy_app/src/lib.rs | 2 + crates/bevy_ecs/src/hierarchy.rs | 43 +--------- 3 files changed, 141 insertions(+), 41 deletions(-) create mode 100644 crates/bevy_app/src/hierarchy.rs diff --git a/crates/bevy_app/src/hierarchy.rs b/crates/bevy_app/src/hierarchy.rs new file mode 100644 index 0000000000000..4dbb6deb15fda --- /dev/null +++ b/crates/bevy_app/src/hierarchy.rs @@ -0,0 +1,137 @@ +use core::marker::PhantomData; + +use bevy_ecs::{ + change_detection::MaybeLocation, + component::Component, + entity::Entity, + hierarchy::ChildOf, + intern::Interned, + lifecycle::Insert, + message::{Message, MessageReader, MessageWriter}, + name::Name, + observer::On, + query::{With, Without}, + schedule::{common_conditions::on_message, IntoScheduleConfigs, ScheduleLabel, SystemSet}, + system::Query, +}; +use bevy_platform::prelude::format; +use bevy_utils::prelude::DebugName; +use log::warn; + +use crate::{Last, Plugin}; + +/// A plugin that verifies that [`Component`] `C` has parents that also have that component. +pub struct ValidateParentHasComponentPlugin { + schedule: Interned, + marker: PhantomData C>, +} + +impl Default for ValidateParentHasComponentPlugin { + fn default() -> Self { + Self::in_schedule(Last) + } +} + +impl ValidateParentHasComponentPlugin { + /// Creates an instance of this plugin that inserts systems in the provided schedule. + pub fn in_schedule(label: impl ScheduleLabel) -> Self { + Self { + schedule: label.intern(), + marker: PhantomData, + } + } +} + +impl Plugin for ValidateParentHasComponentPlugin { + fn build(&self, app: &mut crate::App) { + app.add_message::>() + .add_observer(validate_parent_has_component::) + .add_systems( + self.schedule, + check_parent_has_component:: + .run_if(on_message::>) + .in_set(ValidateParentHasComponentSystems), + ); + } +} + +/// System set for systems added by [`ValidateParentHasComponentPlugin`]. +#[derive(SystemSet, PartialEq, Eq, Hash, Debug, Clone)] +pub struct ValidateParentHasComponentSystems; + +/// An `Insert` observer that when run, will validate that the parent of a given entity contains +/// component `C`. If the parent does not contain `C`, a warning will be logged later in the frame. +fn validate_parent_has_component( + event: On, + child: Query<&ChildOf>, + with_component: Query<(), With>, + mut writer: MessageWriter>, +) { + let Ok(child_of) = child.get(event.entity) else { + return; + }; + if with_component.contains(child_of.parent()) { + return; + } + // This entity may be configured incorrectly, or the parent may just not have been populated + // yet. Send a message to check again later. + writer.write(CheckParentHasComponent:: { + entity: event.entity, + caller: event.caller(), + marker: PhantomData, + }); +} + +/// A message to indicate that this entity should be checked if its parent has a component. +/// +/// While we initially check when emitting these messages, we want to do a second check later on in +/// case the parent eventually gets populated. +#[derive(Message)] +struct CheckParentHasComponent { + /// The entity + entity: Entity, + caller: MaybeLocation, + marker: PhantomData C>, +} + +/// System to handle "check parent" messages and log out any entities that still violate the +/// component hierarchy. +fn check_parent_has_component( + mut messages: MessageReader>, + children: Query<(&ChildOf, Option<&Name>), With>, + components: Query, Without>, +) { + for CheckParentHasComponent { + entity, + caller, + marker: _, + } in messages.read() + { + let Ok((child_of, name)) = children.get(*entity) else { + // Either the entity has been despawned, no longer has `C`, or is no longer a child. In + // any case, we can say that this situation is no longer relevant. + continue; + }; + let parent = child_of.0; + let Ok(parent_name) = components.get(parent) else { + // This can only fail if the parent now has the `C` component. If the parent was + // despawned, the child entity would also be despawned. + continue; + }; + let debug_name = DebugName::type_name::(); + warn!( + "warning[B0004]: {}{name} with the {ty_name} component has a parent ({parent_name}) without {ty_name}.\n\ + This will cause inconsistent behaviors! See: https://bevy.org/learn/errors/b0004", + caller.map(|c| format!("{c}: ")).unwrap_or_default(), + ty_name = debug_name.shortname(), + name = name.map_or_else( + || format!("Entity {entity}"), + |s| format!("The {s} entity") + ), + parent_name = parent_name.map_or_else( + || format!("{parent} entity"), + |s| format!("the {s} entity") + ), + ); + } +} diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index c626c6c73e838..6d5e020dfc12c 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -24,6 +24,7 @@ extern crate alloc; extern crate self as bevy_app; mod app; +mod hierarchy; mod main_schedule; mod panic_handler; mod plugin; @@ -39,6 +40,7 @@ mod terminal_ctrl_c_handler; pub mod hotpatch; pub use app::*; +pub use hierarchy::*; pub use main_schedule::*; pub use panic_handler::*; pub use plugin::*; diff --git a/crates/bevy_ecs/src/hierarchy.rs b/crates/bevy_ecs/src/hierarchy.rs index 7abf918201e4b..89a1a2e725a6e 100644 --- a/crates/bevy_ecs/src/hierarchy.rs +++ b/crates/bevy_ecs/src/hierarchy.rs @@ -12,21 +12,17 @@ use crate::{ bundle::Bundle, component::Component, entity::Entity, - lifecycle::HookContext, - name::Name, relationship::{RelatedSpawner, RelatedSpawnerCommands}, system::EntityCommands, - world::{DeferredWorld, EntityWorldMut, FromWorld, World}, + world::{EntityWorldMut, FromWorld, World}, }; -use alloc::{format, vec::Vec}; +use alloc::vec::Vec; #[cfg(feature = "bevy_reflect")] use bevy_reflect::std_traits::ReflectDefault; #[cfg(all(feature = "serialize", feature = "bevy_reflect"))] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -use bevy_utils::prelude::DebugName; use core::ops::Deref; use core::slice; -use log::warn; /// Stores the parent entity of this child entity with this component. /// @@ -490,41 +486,6 @@ impl<'a> EntityCommands<'a> { } } -/// An `on_insert` component hook that when run, will validate that the parent of a given entity -/// contains component `C`. This will print a warning if the parent does not contain `C`. -pub fn validate_parent_has_component( - world: DeferredWorld, - HookContext { entity, caller, .. }: HookContext, -) { - let entity_ref = world.entity(entity); - let Some(child_of) = entity_ref.get::() else { - return; - }; - let parent = child_of.parent(); - let maybe_parent_ref = world.get_entity(parent); - if let Ok(parent_ref) = maybe_parent_ref - && !parent_ref.contains::() - { - let name = entity_ref.get::(); - let debug_name = DebugName::type_name::(); - let parent_name = parent_ref.get::(); - warn!( - "warning[B0004]: {}{name} with the {ty_name} component has a parent ({parent_name}) without {ty_name}.\n\ - This will cause inconsistent behaviors! See: https://bevy.org/learn/errors/b0004", - caller.map(|c| format!("{c}: ")).unwrap_or_default(), - ty_name = debug_name.shortname(), - name = name.map_or_else( - || format!("Entity {entity}"), - |s| format!("The {s} entity") - ), - parent_name = parent_name.map_or_else( - || format!("{parent} entity"), - |s| format!("the {s} entity") - ), - ); - } -} - /// Returns a [`SpawnRelatedBundle`] that will insert the [`Children`] component, spawn a [`SpawnableList`] of entities with given bundles that /// relate to the [`Children`] entity via the [`ChildOf`] component, and reserve space in the [`Children`] for each spawned entity. /// From d35e66fe3950518733a0a6b174bb324ce7d050cf Mon Sep 17 00:00:00 2001 From: andriyDev Date: Thu, 22 Jan 2026 00:12:46 -0800 Subject: [PATCH 2/4] Replace validate_parent_has_component on InheritedVisibility and GlobalTransform. --- crates/bevy_camera/src/visibility/mod.rs | 8 ++++---- .../src/components/global_transform.rs | 8 ++------ crates/bevy_transform/src/plugins.rs | 16 ++++++++++------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/bevy_camera/src/visibility/mod.rs b/crates/bevy_camera/src/visibility/mod.rs index f8c0faa3f73db..8a70096f53c7f 100644 --- a/crates/bevy_camera/src/visibility/mod.rs +++ b/crates/bevy_camera/src/visibility/mod.rs @@ -10,10 +10,10 @@ use derive_more::derive::{Deref, DerefMut}; pub use range::*; pub use render_layers::*; -use bevy_app::{Plugin, PostUpdate}; +use bevy_app::{Plugin, PostUpdate, ValidateParentHasComponentPlugin}; use bevy_asset::prelude::AssetChanged; use bevy_asset::{AssetEventSystems, Assets}; -use bevy_ecs::{hierarchy::validate_parent_has_component, prelude::*}; +use bevy_ecs::prelude::*; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_transform::{components::GlobalTransform, TransformSystems}; use bevy_utils::{Parallel, TypeIdMap}; @@ -113,7 +113,6 @@ impl PartialEq<&Visibility> for Visibility { /// [`VisibilityPropagate`]: VisibilitySystems::VisibilityPropagate #[derive(Component, Deref, Debug, Default, Clone, Copy, Reflect, PartialEq, Eq)] #[reflect(Component, Default, Debug, PartialEq, Clone)] -#[component(on_insert = validate_parent_has_component::)] pub struct InheritedVisibility(bool); impl InheritedVisibility { @@ -394,7 +393,8 @@ impl Plugin for VisibilityPlugin { fn build(&self, app: &mut bevy_app::App) { use VisibilitySystems::*; - app.register_required_components::() + app.add_plugins(ValidateParentHasComponentPlugin::::default()) + .register_required_components::() .register_required_components::() .register_required_components::() .register_required_components::() diff --git a/crates/bevy_transform/src/components/global_transform.rs b/crates/bevy_transform/src/components/global_transform.rs index cd7db6ef71b09..d97d843011244 100644 --- a/crates/bevy_transform/src/components/global_transform.rs +++ b/crates/bevy_transform/src/components/global_transform.rs @@ -8,7 +8,7 @@ use derive_more::derive::From; use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; #[cfg(feature = "bevy-support")] -use bevy_ecs::{component::Component, hierarchy::validate_parent_has_component}; +use bevy_ecs::component::Component; #[cfg(feature = "bevy_reflect")] use { @@ -47,11 +47,7 @@ use { /// [transform_example]: https://github.com/bevyengine/bevy/blob/latest/examples/transforms/transform.rs #[derive(Debug, PartialEq, Clone, Copy, From)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy-support", - derive(Component), - component(on_insert = validate_parent_has_component::) -)] +#[cfg_attr(feature = "bevy-support", derive(Component))] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), diff --git a/crates/bevy_transform/src/plugins.rs b/crates/bevy_transform/src/plugins.rs index 3881d93c56a3e..50473ecf9ee8e 100644 --- a/crates/bevy_transform/src/plugins.rs +++ b/crates/bevy_transform/src/plugins.rs @@ -1,14 +1,17 @@ -use crate::systems::{ - mark_dirty_trees, propagate_parent_transforms, sync_simple_transforms, - StaticTransformOptimizations, +use crate::{ + prelude::GlobalTransform, + systems::{ + mark_dirty_trees, propagate_parent_transforms, sync_simple_transforms, + StaticTransformOptimizations, + }, }; -use bevy_app::{App, Plugin, PostStartup, PostUpdate}; +use bevy_app::{App, Plugin, PostStartup, PostUpdate, ValidateParentHasComponentPlugin}; use bevy_ecs::schedule::{IntoScheduleConfigs, SystemSet}; /// Set enum for the systems relating to transform propagation #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum TransformSystems { - /// Propagates changes in transform to children's [`GlobalTransform`](crate::components::GlobalTransform) + /// Propagates changes in transform to children's [`GlobalTransform`] Propagate, } @@ -18,7 +21,8 @@ pub struct TransformPlugin; impl Plugin for TransformPlugin { fn build(&self, app: &mut App) { - app.init_resource::() + app.add_plugins(ValidateParentHasComponentPlugin::::default()) + .init_resource::() // add transform systems to startup so the first update is "correct" .add_systems( PostStartup, From 7d8e770d1926062945feeb42c554cf38fd92eec8 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Fri, 23 Jan 2026 18:09:26 -0800 Subject: [PATCH 3/4] Write a migration guide. --- .../validate_parent_has_component_is_now_a_plugin.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 release-content/migration-guides/validate_parent_has_component_is_now_a_plugin.md diff --git a/release-content/migration-guides/validate_parent_has_component_is_now_a_plugin.md b/release-content/migration-guides/validate_parent_has_component_is_now_a_plugin.md new file mode 100644 index 0000000000000..f18f72ea91a9b --- /dev/null +++ b/release-content/migration-guides/validate_parent_has_component_is_now_a_plugin.md @@ -0,0 +1,8 @@ +--- +title: The `validate_parent_has_component` is superseded by `ValidateParentHasComponentPlugin` +pull_requests: [] +--- + +The `validate_parent_has_component` insert hook has been replaced by a plugin: +`ValidateParentHasComponentPlugin`. This uses an observer, a resource, and a system to achieve a +more robust (and less spurious) warning for invalid configuration of entities. From 037f9321154961d4a7481fe63435d2234f9930a9 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sat, 24 Jan 2026 22:23:19 -0800 Subject: [PATCH 4/4] Add PR number to the migration guide. Co-authored-by: Kevin Chen --- .../validate_parent_has_component_is_now_a_plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-content/migration-guides/validate_parent_has_component_is_now_a_plugin.md b/release-content/migration-guides/validate_parent_has_component_is_now_a_plugin.md index f18f72ea91a9b..cf7fa0dd926af 100644 --- a/release-content/migration-guides/validate_parent_has_component_is_now_a_plugin.md +++ b/release-content/migration-guides/validate_parent_has_component_is_now_a_plugin.md @@ -1,6 +1,6 @@ --- title: The `validate_parent_has_component` is superseded by `ValidateParentHasComponentPlugin` -pull_requests: [] +pull_requests: [22675] --- The `validate_parent_has_component` insert hook has been replaced by a plugin: