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-ScenesComposing and serializing ECS objectsC-BugAn unexpected or incorrect behaviorS-Needs-DesignThis issue requires design work to think about how it would best be accomplished

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions