Skip to content

Bundle refactor - Option bundles and Box<dyn Bundle> #19491

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 50 commits into
base: main
Choose a base branch
from

Conversation

SkiFire13
Copy link
Contributor

@SkiFire13 SkiFire13 commented Jun 4, 2025

Objective

Solution

  • Introduce a StaticBundle (name bikesheddable) for usecases where the set of components of a bundle must be knowable without an instance of the bundle and switch functions that needed this aspect of Bundle;
  • Add &self parameters to Bundle's methods to allow them to depend on self's value;
  • (Optimization): add a way to distinguish different types of Bundles:
    • static bundles have all components statically known, like current bundles, and can be cached by their type id;
    • bounded bundles may have some optional components, but overall the set of components they can contain is bounded, allowing to distinguish different subsets by a cache key (in addition to the bundle type id);
    • dynamic bundles have no way to be cached other than looking at their ComponentIds
    • overall this is implemented through the new is_static, is_bounded and cache_key methods on Bundle.
  • Implement Bundle for Option<T: Bundle> as a bounded bundle (i.e. with is_static = false, is_bounded = true and a cache key depending on whether it is Some or None);
  • Make Bundle dyn-compatible and implement Bundle for Box<dyn Bundle>
    • this was done by marking non-dyn compatible methods as where Self: Sized and by introducing a couple of new dyn-compatible automatically-implemented supertraits, BundleDyn and BundleEffectDyn.
  • Add a #[bundle(dynamic)] attribute for the Bundle derive macro to opt-out of deriving StaticBundle and BundleFromComponents
    • BundleFromComponents could theoretically be implemented for bounded but non-static bundles, but it seems tricky and I decided to leave it out from this PR

Testing

  • each new implementation of Bundle comes with its own tests in crates/bevy_ecs/src/bundle.rs
  • benchmarks have been added to validate the performance advantages of the cache key mechanism, see this and this comments

Showcase

fn enemy_bundle(name: String, attack_power: u32, is_boss: bool) -> impl Bundle {
    (
        EnemyMarker,
        Name::new(name),
        AttackPower(attack_power),
        // You can now conditionally insert components or other bundles!
        if is_boss { Some(BossMarker) } else { None }
    )
}

// You can now convert reflected components and bundles into an actual `Bundle` you can insert
fn bundle_from_reflected_data(components: Vec<Box<dyn Reflect>>, registry: &TypeRegistry) -> impl Bundle {
    components
        .into_iter()
        .map(|data| {
            let Some(reflect_bundle) = registry.get_type_data::<ReflectBundle>(data.type_id()) else { todo!() };
            let Ok(bundle_box) = reflect_bundle.get_boxed(data) else { todo!() };
            bundle_box
        })
        .collect::<Vec<Box<dyn Bundle>>>()
}

SkiFire13 added 30 commits June 4, 2025 19:10
@SkiFire13 SkiFire13 marked this pull request as ready for review June 7, 2025 19:33
@alice-i-cecile
Copy link
Member

alice-i-cecile commented Jun 8, 2025

I'd love to see benchmarks for chained / conditional .insert calls: to me that's the real alternative here in terms of performance. We can and should split those up from this PR: those are useful benchmarks for our later command batching work anyways. I still think we should proceed with this regardless of the slow perf, but it's good to be able to test / communicate what the alternatives are.

@SkiFire13
Copy link
Contributor Author

I added a couple more benchmarks to compare with separate inserts, in particular:

  • insert_many/only_last, where I spawn a bunch of components and then insert the last one
  • spawn_many/option_one_none, where I spawn a bunch of components together with a single None
  • spawn_many/option_one_some, where I spawn a bunch of components together with a single Some

These are supposed to simulate the insertion of an optional component, which is currently done with e.g. an insert_if command which performs an additional insert. The results shows that inserting an Option bundle instead is ~2x faster. Note though that the insert_many/only_last emulates the case where the optional component is present. If it isn't then there would be no insert and a result more similar to spawn_many/static is expected (though now that I'm writing this I realized that spawn_many/static is spawning one more component, so it's not a proper comparison).

ecs::bundles::insert_many::insert_many/only_last                                238.1±2.04µs
ecs::bundles::spawn_many::spawn_many/option_one_none                            118.6±1.79µs
ecs::bundles::spawn_many::spawn_many/option_one_some                            124.9±2.17µs

For completeness I also added a benchmark where an insert for each component is called, and as we can expect this is pretty slow:

ecs::bundles::insert_many::insert_many/all                                    1273.3±12.22µs

@alice-i-cecile

@alice-i-cecile
Copy link
Member

Lovely; that's helpful justification, and those benches will be useful in the future.

@SkiFire13
Copy link
Contributor Author

I think the S-Needs-Help, S-Waiting-on-Author and S-Needs-Benchmarking labels could be removed now as the PR should now be ready for review and include benchmarks. I'm not sure how these are generally managed though.

@NthTensor NthTensor removed S-Needs-Benchmarking This set of changes needs performance benchmarking to double-check that they help S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged S-Needs-Help The author needs help finishing this PR. labels Jun 9, 2025
@SkiFire13
Copy link
Contributor Author

Upon seeing #19560 I wonder if StaticBundle should be a separate derive or not. Differently from BundleFromComponents, which is just used in take, StaticBundle is used in a lot of places (almost every API that uses Bundle but not for insert or spawn) so a double derive might be a lot more common. For reference, currently I have a #[bundle(dynamic)] annotation to opt-out of deriving StaticBundle and BundleFromComponents.

@alice-i-cecile
Copy link
Member

I think that making the common case pleasant is the way to go here. Most bundles will be static, so I like your current design.

github-merge-queue bot pushed a commit that referenced this pull request Jun 20, 2025
# Objective

- Splitted off from  #19491
- Make adding generated code to the `Bundle` derive macro easier
- Fix a bug when multiple fields are `#[bundle(ignore)]`

## Solution

- Instead of accumulating the code for each method in a different `Vec`,
accumulate only the names of non-ignored fields and their types, then
use `quote` to generate the code for each of them in the method body.
- To fix the bug, change the code populating the `BundleFieldKind` to
push only one of them per-field (previously each `#[bundle(ignore)]`
resulted in pushing twice, once for the correct
`BundleFieldKind::Ignore` and then again unconditionally for
`BundleFieldKind::Component`)

## Testing

- Added a regression test for the bug that was fixed
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 C-Feature A new feature, making something new possible M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide M-Needs-Release-Note Work that should be called out in the blog due to impact S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Contentious There are nontrivial implications that should be thought through
Projects
Status: Respond (With Priority)
Status: Widget-ready
Development

Successfully merging this pull request may close these issues.

Better tools for working with dynamic collections of components Bundles with optional components
6 participants