Skip to content

Broken entity hierarchy when projecting sub-scene through scene component with names #25407

Description

@nelson137

Bevy version and features

0.19.1

(Testing with default-features = false, features = ["bevy_log", "scene", "debug"] for simplicity but there's no difference in behavior with default features.)

What you did

In a bsn! invocation, have a name on an entity that uses a scene component with projected children that also have names.

All three of those conditions are required to produce the bug:

  • The parent entity has a name
  • The child entity has a name
  • The child entity is "projected" through a scene component
    • i.e. the scene component inserts the scene list from its props into its own scene (props.children below)

Example:

bsn! {
    #Parent
    @Container {
        @children: bsn_list! [ #Child ]
    }
}

Where the scene of Container is just:

bsn! {
    Children [
        { props.children }
    ]
}

This use of BSN can be found at the bottom of the syntax example for the bsn! macro.

What went wrong

The expected entity hierarchy of such a scene is:

  #Parent
  └── #Child

However, spawning this scene results in a broken relationship and misplaced components. The entities are not related at all: one is not a child of the other. Running the app also produces these logs which explains what happened to the parent-child relationship:

2026-08-14T00:00:51.844204Z  WARN bevy_ecs::relationship: The bevy_ecs::hierarchy::ChildOf(30v0) relationship on entity 30v0 points to itself. The invalid bevy_ecs::hierarchy::ChildOf relationship has been removed.
If this is intended behavior self-referential relations can be enabled with the allow_self_referential attribute: #[relationship(allow_self_referential)]

And if we give each entity a marker component in the bsn! to help identify them, then, when the scene spawns, one of the two entities ends up with both markers and the child's name, and the other is not even spawned.

Code
bsn! {
    P
    #Parent
    @Container {
        @children: bsn_list! [ C #Child ]
    }
}

Additional information

I attempted to reduce the example even further, but removing any one of the three previously-mentioned conditions results in the expected parent-child relationship and correctly assigned components.

If no name is given to the parent, then there is no bug:

bsn! {
    @Container {
        @children: bsn_list! [ #Child ]
    }
}

If no name is given to the child, then there is no bug:

bsn! {
    #Parent
    @Container {
        @children: bsn_list! [ () ]
    }
}

If the child is directly related to the parent with Children, and not through the scene component, then there is no bug:

bsn! {
    #Parent
    @Container
    Children [ #Child ]
}

While iterating on the MRE I wrote the following tests:

Full Code
//! ```cargo
//! [dependencies]
//! bevy = { version = "0.19.1", default-features = false, features = ["bevy_log", "scene", "debug"] }
//! ```
use bevy::prelude::*;
/// A scene component that simply spawns an empty entity with the given scene list as its /// `Children`. /// /// A scene declared as: /// /// ```bsn /// @Container { /// @children: bsn_list![ #Child ] /// } /// ``` /// /// creates a hierarchy: /// /// ```plaintext /// (empty) /// └── #Child /// ``` #[derive(SceneComponent, Clone, Default)] #[scene(ContainerProps)] pub struct Container;
impl Container { fn scene(props: ContainerProps) -> impl Scene { bsn! { Children [ { props.children } ] } } }
pub struct ContainerProps { pub children: Box<dyn SceneList>, }
impl Default for ContainerProps { fn default() -> Self { Self { children: Box::new(bsn_list![]), } } }
#[derive(Component, Clone, Default)] struct A;
#[derive(Component, Clone, Default)] struct B;
fn test_app() -> App { let mut app = App::new(); app.add_plugins(( bevy::log::LogPlugin::default(), AssetPlugin::default(), bevy::scene::ScenePlugin, )); app }
/// Checks for expected entity states: /// /// - A: `expected_name_a ChildOf( ) Children[B]` /// - B: `Name("B") ChildOf(A) Children[ ]` #[track_caller] fn check(world: &mut World, expected_name_a: Option<&str>, expected_name_b: Option<&str>) { let a = { let maybe_a = world.query_filtered::<Entity, With<A>>().single(world); assert!(maybe_a.is_ok(), "no entity A: {maybe_a:?}"); maybe_a.unwrap() };
let b = { let maybe_b = world.query_filtered::<Entity, With<B>>().single(world); assert!(maybe_b.is_ok(), "no entity B: {maybe_b:?}"); maybe_b.unwrap() };
assert_ne!(a, b, "A and B are the same entity");
type TestComponents<'a> = (Option<&'a Name>, Option<&'a ChildOf>, Option<&'a Children>); fn get_components(world: &mut World, entity: Entity) -> TestComponents<'_> { world.query::<TestComponents>().get(world, entity).unwrap() }
// Check Entity: A ---
{ let (maybe_name, maybe_child_of, maybe_children) = get_components(world, a);
// A has expected Name match expected_name_a { Some(expected_name) => { assert!(maybe_name.is_some(), "entity A has no Name"); let name = maybe_name.unwrap().as_str(); assert_eq!(name, expected_name, "entity A has unexpected name: {name}"); } None => { assert!( maybe_name.is_none(), "entity A has unexpected name: {:?}", maybe_name.map(Name::as_str) ); } }
// A is not a ChildOf any other entity assert!( maybe_child_of.is_none(), "entity A has unexpected ChildOf: {maybe_child_of:?}" );
// A has child B assert!(maybe_children.is_some(), "entity A has no children"); let maybe_child = maybe_children.unwrap().iter().next(); assert!(maybe_child.is_some(), "entity A has empty children"); let child = maybe_child.unwrap(); assert_ne!(child, a, "entity A is child of itself"); }
// Check Entity: B ---
{ let (maybe_name, maybe_child_of, maybe_children) = get_components(world, b);
// B has expected Name match expected_name_b { Some(expected_name) => { assert!(maybe_name.is_some(), "entity B has no Name"); let name = maybe_name.unwrap().as_str(); assert_eq!(name, expected_name, "entity B has unexpected name: {name}"); } None => { assert!( maybe_name.is_none(), "entity B has unexpected name: {:?}", maybe_name.map(Name::as_str) ); } }
// B is a ChildOf A assert!(maybe_child_of.is_some(), "entity B has no ChildOf"); let child_of = maybe_child_of.unwrap(); assert_eq!( child_of.0, a, "entity B has unexpected ChildOf: {child_of:?}" );
// B has no children assert!( maybe_children.is_none(), "entity B has unexpected children: {maybe_children:?}" ); } }
/// Expected: /// /// - A: `Name("A") ChildOf( ) Children[B]` /// - B: `Name("B") ChildOf(A) Children[ ]` /// /// Actual: /// /// - A,B: `Name("B") ChildOf( ) Children[ ]` /// - (empty) #[test] fn names_on_both() { let mut app = test_app(); let world = app.world_mut(); world .spawn_scene(bsn! { A #A @Container { @children: bsn_list![ #B B ] } }) .unwrap(); check(world, Some("A"), Some("B")); }
/// Expected: /// /// - A: `Name("A") ChildOf( ) Children[B]` /// - B: `Name( ) ChildOf(A) Children[ ]` /// /// Actual: correct #[test] fn name_only_on_scene_component() { let mut app = test_app(); let world = app.world_mut(); world .spawn_scene(bsn! { A #A @Container { @children: bsn_list![ B ] } }) .unwrap(); check(world, Some("A"), None); }
/// Expected: /// /// - A: `Name( ) ChildOf( ) Children[B]` /// - B: `Name("B") ChildOf(A) Children[ ]` /// /// Actual: correct #[test] fn name_only_on_child() { let mut app = test_app(); let world = app.world_mut(); world .spawn_scene(bsn! { A @Container { @children: bsn_list![ #B B ] } }) .unwrap(); check(world, None, Some("B")); }
/// Expected: /// /// - A: `Name( ) ChildOf( ) Children[B]` /// - B: `Name("B") ChildOf(A) Children[ ]` /// /// Actual: correct #[test] fn not_projected() { let mut app = test_app(); let world = app.world_mut(); world .spawn_scene(bsn! { #A A @Container Children [ #B B ] }) .unwrap(); check(world, Some("A"), Some("B")); }
Output
running 4 tests
2026-08-14T03:28:46.325786Z ERROR bevy_log: Could not set global logger and tracing subscriber as they are already set. Consider disabling LogPlugin.
2026-08-14T03:28:46.325787Z ERROR bevy_log: Could not set global logger as it is already set. Consider disabling LogPlugin.
2026-08-14T03:28:46.325789Z ERROR bevy_log: Could not set global tracing subscriber as it is already set. Consider disabling LogPlugin.
2026-08-14T03:28:46.325789Z ERROR bevy_log: Could not set global logger and tracing subscriber as they are already set. Consider disabling LogPlugin.
2026-08-14T03:28:46.327445Z  WARN bevy_ecs::relationship: The bevy_ecs::hierarchy::ChildOf(30v0) relationship on entity 30v0 points to itself. The invalid bevy_ecs::hierarchy::ChildOf relationship has been removed.
If this is intended behavior self-referential relations can be enabled with the allow_self_referential attribute: #[relationship(allow_self_referential)]
test not_projected ... ok
test name_only_on_child ... ok
test name_only_on_scene_component ... ok
test names_on_both ... FAILED
failures:
---- names_on_both stdout ----
thread 'names_on_both' (5263630) panicked at tests/primary.rs:140:5: assertion `left != right` failed: A and B are the same entity left: 30v0 right: 30v0 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures: names_on_both
test result: FAILED. 3 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ScenesComposing and serializing ECS objectsC-BugAn unexpected or incorrect behaviorD-ModestA "normal" level of difficulty; suitable for simple features or challenging fixesS-Ready-For-ImplementationThis issue is ready for an implementation PR. Go for it!

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions