Skip to content

OnAdd events fire before hierarchy has been finalized in 0.16.0-rc.3 #18671

@oliver-dew

Description

@oliver-dew

Bevy version

0.16.0-rc.3

What you did

Bevy 0.15 added observer bubbling/ auto-propagation. This is useful for being able to scope observers to certain branches of a scene hierarchy. Here's an example that converts a non-bubbling event, Trigger<OnAdd> into a bubbling one. The use-case is, spawning various scenes, and scoping observers to those scenes where you react to named components being added (eg, inserting marker components into a gltf hierarchy). Here's a working example from bevy 0.15:

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, spawn_scene)
        .run();
}

fn spawn_scene(mut commands: Commands, assets: Res<AssetServer>) {
    commands.add_observer(bubble_up_onadd_name);

    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0.0, 3.0, 6.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));
    let scene: Handle<Scene> = assets.load("Torus.glb#Scene0");
    commands
        .spawn(SceneRoot(scene))
        .observe(|trigger: Trigger<OnAddName>| {
            println!("bubbled {}", trigger.0); // fires in bevy 0.15, but not in 0.16
        });
}

#[derive(Component)]
struct OnAddName(String);

impl Event for OnAddName {
    type Traversal = &'static Parent;
    const AUTO_PROPAGATE: bool = true;
}

/// convert non-bubbling `OnAdd` into bubbling
fn bubble_up_onadd_name(
    trigger: Trigger<OnAdd, Name>,
    query: Query<&Name>,
    mut commands: Commands,
) {
    let Ok(name) = query.get(trigger.entity()) else { return };
    commands.trigger_targets(OnAddName(name.to_string()), trigger.entity());
    println!("triggered {}", name.to_string());
}

What went wrong

Here's the above code, converted to 0.16 syntax:

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, spawn_scene)
        .run();
}

fn spawn_scene(mut commands: Commands, assets: Res<AssetServer>) {
    commands.add_observer(bubble_up_onadd_name);

    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0.0, 3.0, 6.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));
    let scene: Handle<Scene> = assets.load("Torus.glb#Scene0");
    commands
        .spawn(SceneRoot(scene))
        .observe(|trigger: Trigger<OnAddName>| {
            println!("bubbled {}", trigger.0); // fires in bevy 0.15, but not in 0.16
        });
}

#[derive(Event)]
#[event(traversal = &'static ChildOf, auto_propagate)]
struct OnAddName(String);

fn bubble_up_onadd_name(
    trigger: Trigger<OnAdd, Name>,
    query: Query<&Name>,
    mut commands: Commands,
) {
    let Ok(name) = query.get(trigger.target()) else { return };
    commands.trigger_targets(OnAddName(name.to_string()), trigger.target());
    println!("triggered {}", name.to_string());
}

the event fails to bubble up, likely because the hierarchy isn't finalized yet.

Here's a workaround to achieve the desired functionality. Wait for SceneInstanceReady, then recursively walk the hierarchy to find desired component (in this case Name) and trigger the event. It feels like a bit of a downgrade in ergonomics though:

use bevy::{prelude::*, scene::SceneInstanceReady};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, spawn_scene)
        .run();
}

fn spawn_scene(mut commands: Commands, assets: Res<AssetServer>) {
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0.0, 3.0, 6.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));
    let scene: Handle<Scene> = assets.load("Torus.glb#Scene0");
    commands
        .spawn(SceneRoot(scene))
        .observe(
            |trigger: Trigger<SceneInstanceReady>,
             query: Query<(Option<&Name>, Option<&Children>)>,
             commands: Commands| {
                recursively_seek_name(trigger.target(), query, commands);
            },
        )
        .observe(|trigger: Trigger<OnAddName>| {
            println!("bubbled {}", trigger.0);
        });
}

fn recursively_seek_name(
    entity: Entity,
    query: Query<(Option<&Name>, Option<&Children>)>,
    mut commands: Commands,
) {
    let (maybe_name, maybe_children) = query.get(entity).unwrap();
    if let Some(name) = maybe_name {
        commands.trigger_targets(OnAddName(name.to_string()), entity);
    }
    if let Some(children) = maybe_children {
        for child in children {
            recursively_seek_name(*child, query, commands.reborrow());
        }
    }
}
#[derive(Event)]
#[event(traversal = &'static ChildOf, auto_propagate)]
struct OnAddName(String);

TLDR

OnAdd firing before the hierarchy (and possibly other relationship types) have been finalized reduces the usefulness of relationship traversing features such as auto-propagating events

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ECSEntities, components, systems, and eventsA-ScenesSerialized ECS data stored on the diskC-BugAn unexpected or incorrect behaviorS-Needs-DesignThis issue requires design work to think about how it would best be accomplished

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions