Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add a setup Component derive helper#23093

Open
Runi-c wants to merge 1 commit intobevyengine:mainfrom
Runi-c:component_setup_helper
Open

Add a setup Component derive helper#23093
Runi-c wants to merge 1 commit intobevyengine:mainfrom
Runi-c:component_setup_helper

Conversation

@Runi-c
Copy link
Contributor

@Runi-c Runi-c commented Feb 21, 2026

Objective

  • A very common pattern has emerged since Observers were added where an On<Add, Component> observer is used to "set up" an entity prototype based on a marker struct:
pub fn plugin(app: &mut App) {
    app.add_observer(setup);
}
#[derive(Component)]
pub struct MyComponent;
fn setup(evt: On<Add, MyComponent>, mut commands: Commands) {
    commands
        .entity(evt.entity)
        .insert((
            // insert some other components, maybe fetch
            // data from a query, use the asset server, etc...
        ))
        .observe(some_other_observer);
}
  • Looking through open-source repos from the Bevy Jam 7, I see this exact pattern multiple times in nearly every repo. It's really useful!
    • In my own submission, 42 out of 80 plugin functions consist solely of adding one such setup observer.
  • We can make this more ergonomic!

Solution

Add a setup derive helper to the Component derive helper so that devs can statically declare setup systems without needing to add any plugin machinery.

Testing

Tested in a sample project and it works as expected.
Helper expands to a hook like this:

fn on_add() -> ::core::option::Option<::bevy::ecs::lifecycle::ComponentHook> {
    ::core::option::Option::Some(|mut world, ctx| {
        world.commands().run_system_cached_with(setup, ctx.entity);
        on_add(world, ctx)
    })
}

where the on_add(world, ctx) is the custom on_add hook set via #[component(on_add = ...)] or omitted if none is set.


Showcase

#[derive(Component)]
#[setup(setup)]
pub struct MyComponent;
fn setup(entity: In<Entity>, mut commands: Commands) {
    commands
        .entity(*entity)
        .insert((
            // insert some other components, maybe fetch
            // data from a query, use the asset server, etc...
        ))
        .observe(some_other_observer);
}

Note

A-OK if this isn't a direction that's wanted, just figured I'd throw the PR out there because if it goes through I'd use it to remove 42 plugins from my code!

Copy link
Contributor

@Jondolf Jondolf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem too much better than just writing out the hook manually:

#[derive(Component)]
#[component(on_add)]
pub struct MyComponent;

impl MyComponent {
    fn on_add(world: DeferredWorld, ctx: HookContext) {
        todo!(); // you could also run a one-shot system here if you wanted to
    }
}

I also don't like that setup implicitly adds the on_add implementation, without clearly implying it in the name. I can picture someone trying to add both and getting errors.

Another potential footgun here is that the setup method runs a one-shot system with a command, so it's deferred, and doesn't run immediately when the component is added the way on_add does.

Having something like setup also makes me wonder, where's the equivalent for on_remove? Having sugar for just a single hook seems a bit inconsistent to me

@Jondolf Jondolf added D-Trivial Nice and easy! A great choice to get started with Bevy A-ECS Entities, components, systems, and events X-Contentious There are nontrivial implications that should be thought through S-Needs-Review Needs reviewer attention (from anyone!) to move forward D-Macros Code that generates Rust code labels Feb 21, 2026
@github-project-automation github-project-automation bot moved this to Needs SME Triage in ECS Feb 21, 2026
@Runi-c
Copy link
Contributor Author

Runi-c commented Feb 21, 2026

This doesn't seem too much better than just writing out the hook manually

I think it's a lot better than writing out the hook manually, as that doesn't get you systemparams, complicates resource access due to aliasing rules (like having Assets and AssetServer at the same time), and adds extra lines to do anything appreciable. I think it's more in competition with the setup observer pattern which is what everyone seems to be leaning towards.

Compare:

#[derive(Component)]
#[component(on_add)]
pub struct Test;
impl Test {
    fn on_add(mut world: DeferredWorld, ctx: HookContext) {
        // these must be ordered like this to avoid mutably borrowing world twice!
        let asset_server = world.resource::<AssetServer>();
        let mesh = asset_server.load("mesh.glb"); 
        let mut materials = world.resource_mut::<Assets<StandardMaterial>>();
        let material = materials.add(Color::WHITE);
        world
            .commands()
            .entity(ctx.entity)
            .insert((Mesh3d(mesh), MeshMaterial3d(material)))
            .observe(my_observer);
    }
}
#[derive(Component)]
#[setup(setup_test)]
pub struct Test;
fn setup_test(
    entity: In<Entity>,
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    // freedom!
    commands
        .entity(*entity)
        .insert((
            Mesh3d(asset_server.load("mesh.glb")),
            MeshMaterial3d(materials.add(Color::WHITE)),
        ))
        .observe(my_observer);
}

I also don't like that setup implicitly adds the on_add implementation, without clearly implying it in the name. I can picture someone trying to add both and getting errors.

As implemented in this PR you can have both and they get combined without errors (see the macro expansion)

Another potential footgun here is that the setup method runs a one-shot system with a command, so it's deferred

Yup that seems like a potential issue, fair enough.

Having something like setup also makes me wonder, where's the equivalent for on_remove?

Destructor patterns seem to be way less common but I could add sugar for that too for consistency!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ECS Entities, components, systems, and events D-Macros Code that generates Rust code D-Trivial Nice and easy! A great choice to get started with Bevy S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Contentious There are nontrivial implications that should be thought through

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

2 participants