Skip to content

Commit 6b1be3e

Browse files
committed
Add warning when a hierarchy component is missing (#5590)
# Objective A common pitfall since 0.8 is the requirement on `ComputedVisibility` being present on all ancestors of an entity that itself has `ComputedVisibility`, without which, the entity becomes invisible. I myself hit the issue and got very confused, and saw a few people hit it as well, so it makes sense to provide a hint of what to do when such a situation is encountered. - Fixes #5849 - Closes #5616 - Closes #2277 - Closes #5081 ## Solution We now check that all entities with both a `Parent` and a `ComputedVisibility` component have parents that themselves have a `ComputedVisibility` component. Note that the warning is only printed once. We also add a similar warning to `GlobalTransform`. This only emits a warning. Because sometimes it could be an intended behavior. Alternatives: - Do nothing and keep repeating to newcomers how to avoid recurring pitfalls - Make the transform and visibility propagation tolerant to missing components (#5616) - Probably archetype invariants, though the current draft would not allow detecting that kind of errors --- ## Changelog - Add a warning when encountering dubious component hierarchy structure Co-authored-by: Nicola Papale <[email protected]>
1 parent e33d7be commit 6b1be3e

File tree

7 files changed

+225
-2
lines changed

7 files changed

+225
-2
lines changed

crates/bevy_hierarchy/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ trace = []
1414
[dependencies]
1515
# bevy
1616
bevy_app = { path = "../bevy_app", version = "0.9.0-dev" }
17+
bevy_core = { path = "../bevy_core", version = "0.9.0-dev" }
1718
bevy_ecs = { path = "../bevy_ecs", version = "0.9.0-dev", features = ["bevy_reflect"] }
19+
bevy_log = { path = "../bevy_log", version = "0.9.0-dev" }
1820
bevy_reflect = { path = "../bevy_reflect", version = "0.9.0-dev", features = ["bevy"] }
1921
bevy_utils = { path = "../bevy_utils", version = "0.9.0-dev" }
2022

crates/bevy_hierarchy/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@ pub use child_builder::*;
1616
mod events;
1717
pub use events::*;
1818

19+
mod valid_parent_check_plugin;
20+
pub use valid_parent_check_plugin::*;
21+
1922
#[doc(hidden)]
2023
pub mod prelude {
2124
#[doc(hidden)]
22-
pub use crate::{child_builder::*, components::*, hierarchy::*, HierarchyPlugin};
25+
pub use crate::{
26+
child_builder::*, components::*, hierarchy::*, HierarchyPlugin, ValidParentCheckPlugin,
27+
};
2328
}
2429

2530
use bevy_app::prelude::*;
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use std::marker::PhantomData;
2+
3+
use bevy_app::{App, CoreStage, Plugin};
4+
use bevy_core::Name;
5+
use bevy_ecs::{prelude::*, schedule::ShouldRun};
6+
use bevy_log::warn;
7+
use bevy_utils::{get_short_name, HashSet};
8+
9+
use crate::Parent;
10+
11+
/// When enabled, runs [`check_hierarchy_component_has_valid_parent<T>`].
12+
///
13+
/// This resource is added by [`ValidParentCheckPlugin<T>`].
14+
/// It is enabled on debug builds and disabled in release builds by default,
15+
/// you can update this resource at runtime to change the default behavior.
16+
#[derive(Resource)]
17+
pub struct ReportHierarchyIssue<T> {
18+
/// Whether to run [`check_hierarchy_component_has_valid_parent<T>`].
19+
pub enabled: bool,
20+
_comp: PhantomData<fn(T)>,
21+
}
22+
impl<T> Default for ReportHierarchyIssue<T> {
23+
fn default() -> Self {
24+
Self {
25+
enabled: cfg!(debug_assertions),
26+
_comp: PhantomData,
27+
}
28+
}
29+
}
30+
31+
/// System to print a warning for each `Entity` with a `T` component
32+
/// which parent hasn't a `T` component.
33+
///
34+
/// Hierarchy propagations are top-down, and limited only to entities
35+
/// with a specific component (such as `ComputedVisibility` and `GlobalTransform`).
36+
/// This means that entities with one of those component
37+
/// and a parent without the same component is probably a programming error.
38+
/// (See B0004 explanation linked in warning message)
39+
pub fn check_hierarchy_component_has_valid_parent<T: Component>(
40+
parent_query: Query<
41+
(Entity, &Parent, Option<&Name>),
42+
(With<T>, Or<(Changed<Parent>, Added<T>)>),
43+
>,
44+
component_query: Query<(), With<T>>,
45+
mut already_diagnosed: Local<HashSet<Entity>>,
46+
) {
47+
for (entity, parent, name) in &parent_query {
48+
let parent = parent.get();
49+
if !component_query.contains(parent) && !already_diagnosed.contains(&entity) {
50+
already_diagnosed.insert(entity);
51+
warn!(
52+
"warning[B0004]: {name} with the {ty_name} component has a parent without {ty_name}.\n\
53+
This will cause inconsistent behaviors! See https://bevyengine.org/learn/errors/#B0004",
54+
ty_name = get_short_name(std::any::type_name::<T>()),
55+
name = name.map_or("An entity".to_owned(), |s| format!("The {s} entity")),
56+
);
57+
}
58+
}
59+
}
60+
61+
/// Run criteria that only allows running when [`ReportHierarchyIssue<T>`] is enabled.
62+
pub fn on_hierarchy_reports_enabled<T>(report: Res<ReportHierarchyIssue<T>>) -> ShouldRun
63+
where
64+
T: Component,
65+
{
66+
report.enabled.into()
67+
}
68+
69+
/// Print a warning for each `Entity` with a `T` component
70+
/// whose parent doesn't have a `T` component.
71+
///
72+
/// See [`check_hierarchy_component_has_valid_parent`] for details.
73+
pub struct ValidParentCheckPlugin<T: Component>(PhantomData<fn() -> T>);
74+
impl<T: Component> Default for ValidParentCheckPlugin<T> {
75+
fn default() -> Self {
76+
Self(PhantomData)
77+
}
78+
}
79+
80+
impl<T: Component> Plugin for ValidParentCheckPlugin<T> {
81+
fn build(&self, app: &mut App) {
82+
app.init_resource::<ReportHierarchyIssue<T>>()
83+
.add_system_to_stage(
84+
CoreStage::Last,
85+
check_hierarchy_component_has_valid_parent::<T>
86+
.with_run_criteria(on_hierarchy_reports_enabled::<T>),
87+
);
88+
}
89+
}

crates/bevy_render/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ mod spatial_bundle;
1818
pub mod texture;
1919
pub mod view;
2020

21+
use bevy_hierarchy::ValidParentCheckPlugin;
2122
pub use extract_param::Extract;
2223

2324
pub mod prelude {
@@ -34,6 +35,7 @@ pub mod prelude {
3435
}
3536

3637
pub use once_cell;
38+
use prelude::ComputedVisibility;
3739

3840
use crate::{
3941
camera::CameraPlugin,
@@ -315,7 +317,8 @@ impl Plugin for RenderPlugin {
315317
});
316318
}
317319

318-
app.add_plugin(WindowRenderPlugin)
320+
app.add_plugin(ValidParentCheckPlugin::<ComputedVisibility>::default())
321+
.add_plugin(WindowRenderPlugin)
319322
.add_plugin(CameraPlugin)
320323
.add_plugin(ViewPlugin)
321324
.add_plugin(MeshPlugin)

crates/bevy_transform/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub mod prelude {
1414

1515
use bevy_app::prelude::*;
1616
use bevy_ecs::prelude::*;
17+
use bevy_hierarchy::ValidParentCheckPlugin;
1718
use prelude::{GlobalTransform, Transform};
1819

1920
/// A [`Bundle`] of the [`Transform`] and [`GlobalTransform`]
@@ -86,6 +87,7 @@ impl Plugin for TransformPlugin {
8687
fn build(&self, app: &mut App) {
8788
app.register_type::<Transform>()
8889
.register_type::<GlobalTransform>()
90+
.add_plugin(ValidParentCheckPlugin::<GlobalTransform>::default())
8991
// add transform systems to startup so the first update is "correct"
9092
.add_startup_system_to_stage(
9193
StartupStage::PostStartup,

errors/B0004.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# B0004
2+
3+
A runtime warning.
4+
5+
An [`Entity`] with a hierarchy-inherited component has a [`Parent`]
6+
without the hierarchy-inherited component in question.
7+
8+
The hierarchy-inherited components defined in bevy include:
9+
10+
- [`ComputedVisibility`]
11+
- [`GlobalTransform`]
12+
13+
Third party plugins may also define their own hierarchy components, so
14+
read the warning message carefully and pay attention to the exact type
15+
of the missing component.
16+
17+
To fix this warning, add the missing hierarchy component to all ancestors
18+
of entities with the hierarchy component you wish to use.
19+
20+
The following code will cause a warning to be emitted:
21+
22+
```rust,no_run
23+
use bevy::prelude::*;
24+
25+
// WARNING: this code is buggy
26+
fn setup_cube(
27+
mut commands: Commands,
28+
mut meshes: ResMut<Assets<Mesh>>,
29+
mut materials: ResMut<Assets<StandardMaterial>>,
30+
) {
31+
commands
32+
.spawn_bundle(TransformBundle::default())
33+
.with_children(|parent| {
34+
// cube
35+
parent.spawn_bundle(PbrBundle {
36+
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
37+
material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()),
38+
transform: Transform::from_xyz(0.0, 0.5, 0.0),
39+
..default()
40+
});
41+
});
42+
43+
// camera
44+
commands.spawn_bundle(Camera3dBundle {
45+
transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
46+
..default()
47+
});
48+
}
49+
50+
fn main() {
51+
App::new()
52+
.add_plugins(DefaultPlugins)
53+
.add_startup_system(setup_cube)
54+
.run();
55+
}
56+
```
57+
58+
This code **will not** show a cube on screen.
59+
This is because the entity spawned with `commands.spawn_bundle(…)`
60+
doesn't have a [`ComputedVisibility`] component.
61+
Since the cube is spawned as a child of an entity without the
62+
[`ComputedVisibility`] component, it will not be visible at all.
63+
64+
To fix this, you must use [`SpatialBundle`] over [`TransformBundle`],
65+
as follows:
66+
67+
```rust,no_run
68+
use bevy::prelude::*;
69+
70+
fn setup_cube(
71+
mut commands: Commands,
72+
mut meshes: ResMut<Assets<Mesh>>,
73+
mut materials: ResMut<Assets<StandardMaterial>>,
74+
) {
75+
commands
76+
// We use SpatialBundle instead of TransformBundle, it contains the
77+
// ComputedVisibility component needed to display the cube,
78+
// In addition to the Transform and GlobalTransform components.
79+
.spawn_bundle(SpatialBundle::default())
80+
.with_children(|parent| {
81+
// cube
82+
parent.spawn_bundle(PbrBundle {
83+
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
84+
material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()),
85+
transform: Transform::from_xyz(0.0, 0.5, 0.0),
86+
..default()
87+
});
88+
});
89+
90+
// camera
91+
commands.spawn_bundle(Camera3dBundle {
92+
transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
93+
..default()
94+
});
95+
}
96+
97+
fn main() {
98+
App::new()
99+
.add_plugins(DefaultPlugins)
100+
.add_startup_system(setup_cube)
101+
.run();
102+
}
103+
```
104+
105+
A similar problem occurs when the [`GlobalTransform`] component is missing.
106+
However, when a parent [`GlobalTransform`] is missing,
107+
it will simply prevent all transform propagation,
108+
including when updating the [`Transform`] component of the child.
109+
110+
You will most likely encounter this warning when loading a scene
111+
as a child of a pre-existing [`Entity`] that does not have the proper components.
112+
113+
[`ComputedVisibility`]: https://docs.rs/bevy/*/bevy/render/view/struct.ComputedVisibility.html
114+
[`GlobalTransform`]: https://docs.rs/bevy/*/bevy/transform/components/struct.GlobalTransform.html
115+
[`Transform`]: https://docs.rs/bevy/*/bevy/transform/components/struct.Transform.html
116+
[`Parent`]: https://docs.rs/bevy/*/bevy/hierarchy/struct.Parent.html
117+
[`Entity`]: https://docs.rs/bevy/*/bevy/ecs/entity/struct.Entity.html
118+
[`SpatialBundle`]: https://docs.rs/bevy/*/bevy/render/prelude/struct.SpatialBundle.html
119+
[`TransformBundle`]: https://docs.rs/bevy/*/bevy/transform/struct.TransformBundle.html

errors/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ pub struct B0002;
66

77
#[doc = include_str!("../B0003.md")]
88
pub struct B0003;
9+
10+
#[doc = include_str!("../B0004.md")]
11+
pub struct B0004;

0 commit comments

Comments
 (0)