Skip to content
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

Extend cloning functionality and add convenience methods to EntityWorldMut and EntityCommands #16826

Merged
merged 3 commits into from
Dec 16, 2024

Conversation

JaySpruce
Copy link
Contributor

@JaySpruce JaySpruce commented Dec 15, 2024

Objective

Thanks to @eugineerd's work on entity cloning (#16132), we now have a robust way to copy components between entities. We can extend this to implement some useful functionality that would have been more complicated before.

Closes #15350.

Solution

EntityCloneBuilder now automatically includes required components alongside any component added to or removed from the component filter.

Added the following methods to EntityCloneBuilder:

  • move_components
    • Syntax: move_components(&mut self, enable: bool)
    • Enables/disables removing the components from the original entity at the end of the cloning process, effectively moving them to the target entity.
  • without_required_components
    • Syntax: without_required_components(&mut self, builder: impl FnOnce(&mut EntityCloneBuilder))
    • Opens a scope where any components added to or removed from the filter will not automatically involve those components' required components.

Added the following methods to EntityWorldMut and EntityCommands:

  • clone_with
    • Syntax: clone_with(&mut self, target: Entity, config: impl FnOnce(&mut EntityCloneBuilder))
    • Easy access to EntityCloneBuilder.
  • clone_components
    • Syntax: clone_components::<B: Bundle>(&mut self, target: Entity)
    • Shortcut if you just want to clone components with default settings.
  • move_components
    • Syntax: move_components::<B: Bundle>(&mut self, target: Entity)
    • Shortcut if you want to move components with otherwise default settings.

Also added clone_and_spawn and clone_and_spawn_with to EntityWorldMut (EntityCommands already had them).

Showcase

assert_eq!(world.entity(entity_a).get::<ComponentA>(), Some(&ComponentA(1)));
assert_eq!(world.entity(entity_b).get::<ComponentA>(), None);
world.entity_mut(entity_a).clone_components::<ComponentA>(entity_b);
assert_eq!(world.entity(entity_a).get::<ComponentA>(), Some(&ComponentA(1)));
assert_eq!(world.entity(entity_b).get::<ComponentA>(), Some(&ComponentA(1)));

assert_eq!(world.entity(entity_a).get::<ComponentB>(), Some(&ComponentB(2)));
assert_eq!(world.entity(entity_b).get::<ComponentB>(), None);
world.entity_mut(entity_a).move_components::<ComponentB>(entity_b);
assert_eq!(world.entity(entity_a).get::<ComponentB>(), None);
assert_eq!(world.entity(entity_b).get::<ComponentB>(), Some(&ComponentB(2)));

@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use 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 labels Dec 15, 2024
@alice-i-cecile alice-i-cecile modified the milestones: 0.15.1, 0.16 Dec 15, 2024
Copy link
Contributor

@eugineerd eugineerd left a comment

Choose a reason for hiding this comment

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

I would suggest moving most of the functionality to EntityCloneBuilder which should allow to use it in clone_and_spawn methods as well. Also I'm a bit worried about the combinatorial explosion of methods - that is why I made EntityCloneBuilder in the first place. ( Now It's my time to nitpick :P )

Comment on lines 2201 to 2220
pub fn clone_components_with_requires<B: Bundle>(&mut self, target: Entity) -> &mut Self {
self.assert_not_despawned();

let storages = &mut self.world.storages;
let components = &mut self.world.components;
let bundles = &mut self.world.bundles;

let bundle_id = bundles.register_contributed_bundle_info::<B>(components, storages);
// SAFETY: the `BundleInfo` for this `BundleId` is initialized above
let bundle_info = unsafe { bundles.get_unchecked(bundle_id) };
let component_ids = bundle_info.contributed_components().to_owned();

let mut builder = EntityCloneBuilder::new(self.world);
builder.deny_all().allow_by_ids(component_ids);
builder.clone_entity(self.entity, target);

self.world.flush();
self.update_location();
self
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm pretty sure allowing and denying components with their required components can be done as a mode for EntityCloneBuilder, which should be more flexible:

builder.with_required_components(|builder| {
  builder
    .allow::<B>()       // Calling with_required_components can set an internal flag which would
    .allow_by_ids(ids); // make all allow* and deny* methods also include contributed_components
});

Copy link
Contributor Author

@JaySpruce JaySpruce Dec 15, 2024

Choose a reason for hiding this comment

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

Something like this?

Code
pub fn with_required_components(
    &mut self,
    f: impl FnOnce(&mut EntityCloneBuilder) + Send + Sync + 'static,
) -> &mut Self {
    self.attach_required_components = true;
    f(self);
    self.attach_required_components = false;
    self
}

pub fn allow<T: Bundle>(&mut self) -> &mut Self {
    if self.filter_allows_components {
        T::get_component_ids(self.world.components(), &mut |id| {
            if let Some(id) = id {
                self.filter.insert(id);
                if self.attach_required_components {
                    if let Some(info) = self.world.components().get_info(id) {
                        for required in info.required_components().iter_ids() {
                            self.filter.insert(required);
                        }
                    }
                }
            }
        });
    } else {
        T::get_component_ids(self.world.components(), &mut |id| {
            if let Some(id) = id {
                self.filter.remove(&id);
                if self.attach_required_components {
                    if let Some(info) = self.world.components().get_info(id) {
                        for required in info.required_components().iter_ids() {
                            self.filter.remove(&required);
                        }
                    }
                }
            }
        });
    }
    self
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this code can actually be deduplicated a bit if we replace if self.filter_allows_components with something like this:

fn apply_filter(
    &mut self,
    id: ComponentId,
) {
    if self.filter_allows_components {
        self.filter.insert(id);
    } else {
        self.filter.remove(&id);
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can't get around the borrow checker to get that to work, but moving the ifs around is still an improvement

Comment on lines 2259 to 2277
pub fn move_components<B: Bundle>(&mut self, target: Entity) -> &mut Self {
self.assert_not_despawned();

let mut builder = EntityCloneBuilder::new(self.world);
builder.deny_all().allow::<B>();
builder.clone_entity(self.entity, target);

let storages = &mut self.world.storages;
let components = &mut self.world.components;
let bundles = &mut self.world.bundles;
let bundle_id = bundles.register_info::<B>(components, storages);

// SAFETY: the `BundleInfo` for this `BundleId` is initialized above
unsafe { self.remove_bundle(bundle_id) };

self.world.flush();
self.update_location();
self
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Moving components can also be a configuration option for EntityCloneBuilder that would call remove_bundle as a final step in clone_entity. Theoretically, it would also allow us to add an optimization in the future that would allow EntityCloner to move components that don't implement Clone or Reflect.

///
/// - If this entity has been despawned while this `EntityWorldMut` is still alive.
/// - If the target entity does not exist.
pub fn move_components_with_requires<B: Bundle>(&mut self, target: Entity) -> &mut Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we can avoid combinatorial explosion of methods by just having move_components and move_components_with similar to spawn_and_clone? Although I'm not sure if configuring through EntityCloneBuilder is a good idea - it would allow to set cloning hierarchy which would not make much sense in the context of cloning components from one entity to the next. Maybe there should be a ComponentCloneBuilder or something similar that would re-expose only the methods that should be allowed?

@JaySpruce
Copy link
Contributor Author

All true, I think I'll cut it down to these:

  • clone_with (gives plain access to the builder)
  • clone_and_spawn
  • clone_and_spawn_with
  • clone_components
  • move_components

I'll add the required components stuff to the builder, and tinker with the moving functionality too

@alice-i-cecile alice-i-cecile added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Dec 15, 2024
@alice-i-cecile
Copy link
Member

I really like this direction, and @eugineerd's feedback is excellent. Ping me when you need a review please :)

@JaySpruce JaySpruce changed the title Add clone_components and move_components (and variants) to EntityWorldMut and EntityCommands Extend cloning functionality and add convenience methods to EntityWorldMut and EntityCommands Dec 16, 2024
@JaySpruce
Copy link
Contributor Author

I added the helper functions for the filter, so the allow/deny methods are slimmer now.

Required components work as discussed, the with_required_components method opening a sub-builder thing that includes required components in any allow/deny.

Moving components is just a bool in EntityCloneBuilder, which enables or disables removing all components that were cloned at the end of clone_entity.

This is the full syntax:

world.entity_mut(entity_a).clone_with(entity_b, |builder| {
    builder.deny_all();
    builder.enable_move_on_clone();
    builder.with_required_components(|builder| {
        builder.allow::<B>();
    });
});

Also added EntityWorldMut::remove_by_ids because I needed it (there was remove_by_id, so remove matches insert now, which has insert_by_id and insert_by_ids).

I think we're good to go for round 2, @alice-i-cecile & @eugineerd.

}
.clone_entity(world);

world.flush_commands();
}

/// Allows for a scoped mode where any changes to the filter that allow/deny components
/// will also allow/deny those components' required components, recursively.
pub fn with_required_components(
Copy link
Member

Choose a reason for hiding this comment

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

Very cool. @cart is almost certainly going to want to want this on by default, possibly without the ability to disable it. The vision is to treat these as "one indivisible object" wherever possible, at least for "required" components :)

I agree with that philosophy, and I think the toggle should basically be "replace required components with their default values". The default value should be "copy all required components that you can".

Copy link
Contributor

Choose a reason for hiding this comment

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

A bit off-topic for this PR, but I wonder if DynamicSceneBuilder should also extract required components in that case?

Copy link
Contributor Author

@JaySpruce JaySpruce Dec 16, 2024

Choose a reason for hiding this comment

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

That's essentially what the toggle is, since components are ultimately inserted with normal insert methods. I can switch the default though.

Here's a couple colors for the bikeshed:

  • default_required_components
  • with_default_required_components
  • replace_required_components
  • replace_required_components_with_default
  • without_required_components

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, we should probably be unifying the mechanisms between this and the scenes stuff. Another PR / issue though! As for the bikeshed, go with the long and clear one please: this is quite niche :)

self
}

/// The default setting for the cloner. The source entity will keep
Copy link
Member

Choose a reason for hiding this comment

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

The bit about this being the default should be in the second line, not the first.

///
/// # Panics
///
/// The command will panic when applied if either of the entities do not exist.
Copy link
Member

Choose a reason for hiding this comment

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

Gah more panics :( Not your problem: this needs a holistic solution.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I do have another PR for that :P (not holistic, just commands). Not sure how it'll interact with work in the fallible systems area though

/// [`Clone`] or [`Reflect`](bevy_reflect::Reflect).
///
/// Configure through [`EntityCloneBuilder`] as follows:
/// ```ignore
Copy link
Member

Choose a reason for hiding this comment

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

Don't ignore this: make it work (possibly hiding lines for importing dependencies / boilerplate). Ignored doc tests are extremely brittle.

///
/// This only applies to components that are allowed through the filter
/// at the time [`EntityCloneBuilder::clone_entity`] is called.
pub fn enable_move_on_clone(&mut self) -> &mut Self {
Copy link
Member

Choose a reason for hiding this comment

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

Bikeshed: I think these methods might be clearer if they were move_components and keep_components. The enable/disable terminology was confusing to me at first.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should just be one method: move_components that takes a bool. That way it would be in line with the other EntityCloneBuilder options (like add_observers). It was designed to look somewhat similar to std::fs::OpenOptions.

Copy link
Contributor

@eugineerd eugineerd left a comment

Choose a reason for hiding this comment

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

Looks good to me! I like the addition of various convenience methods and move component functionality is pretty useful.

I have also prepared an optimization based on code from #16717 for move_components that would allow us to move components that don't implement Reflect or Clone after both PRs are merged.

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

I'm happy with the state of this now; let's merge and refine in follow-up :)

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Dec 16, 2024
Merged via the queue into bevyengine:main with commit 5a94beb Dec 16, 2024
28 checks passed
@JaySpruce JaySpruce deleted the moving_and_cloning branch December 16, 2024 22:15
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-Usability A targeted quality-of-life change that makes Bevy easier to use M-Needs-Release-Note Work that should be called out in the blog due to impact S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Moving components from one entity to another
3 participants