Skip to content

Hooks/observers for require components should be invoked in a predictable order. #20001

@Aqaao

Description

@Aqaao

What problem does this solve or what need does it fill?

In the current entity spawn process, hooks/observers for components that are added directly are executed in the order they written in code.
However, the hooks/observers of their require components are executed in a completely non-deterministic order.

What solution would you like?

It might be better to invoke hook/observer according to declaration order of the Require components.

What alternative(s) have you considered?

There's no way to control the execution order of hooks/observers for require components.

Additional context

use std::fmt::Debug;

use bevy::ecs::{component::Component, lifecycle::HookContext, world::{DeferredWorld, World}};

fn main() {
    let mut world = World::default();
    world.spawn(ABC);
    world.spawn(GFEDCBA);

}

fn print_hook<T: Component + Debug>(
    world: DeferredWorld,
    HookContext { entity, ..}: HookContext,
){
    println!("on_add {:?}",world.get::<T>(entity));
}

#[derive(Component, Default, Debug)]
#[component(on_add = print_hook::<A>)]
struct A;

#[derive(Component, Default, Debug)]
#[component(on_add = print_hook::<B>)]
struct B;

#[derive(Component, Default, Debug)]
#[component(on_add = print_hook::<C>)]
struct C;

#[derive(Component, Default, Debug)]
#[component(on_add = print_hook::<D>)]
struct D;

#[derive(Component, Default, Debug)]
#[component(on_add = print_hook::<E>)]
struct E;

#[derive(Component, Default, Debug)]
#[component(on_add = print_hook::<F>)]
struct F;

#[derive(Component, Default, Debug)]
#[component(on_add = print_hook::<G>)]
struct G;

#[derive(Component, Default, Debug)]
#[component(on_add = print_hook::<ABC>)]
#[require(A,B,C)]
struct ABC;

#[derive(Component, Default, Debug)]
#[component(on_add = print_hook::<GFEDCBA>)]
#[require(G,F,E,D,C,B,A)]
struct GFEDCBA;

output:

on_add Some(ABC)
on_add Some(B)
on_add Some(C)
on_add Some(A)
on_add Some(GFEDCBA)
on_add Some(E)
on_add Some(B)
on_add Some(F)
on_add Some(C)
on_add Some(D)
on_add Some(G)
on_add Some(A)

let required_components = required_components
.0
.into_iter()
.map(|(component_id, v)| {
// Safety: These ids came out of the passed `components`, so they must be valid.
let info = unsafe { components.get_info_unchecked(component_id) };
storages.prepare_component(info);
// This adds required components to the component_ids list _after_ using that list to remove explicitly provided
// components. This ordering is important!
component_ids.push(component_id);
v.constructor
})
.collect();

The reason is that BundleInfo::component_ids gets required component IDs from RequiredComponents, which internally uses a HashMap for storage.

I'm not sure if this is a bug, so I'm posting issue here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ECSEntities, components, systems, and eventsC-FeatureA new feature, making something new possibleC-UsabilityA targeted quality-of-life change that makes Bevy easier to use

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions