-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Entity cloning #16132
base: main
Are you sure you want to change the base?
Entity cloning #16132
Conversation
I have wanted this feature for years. Thank you so much for taking a crack at it! I don't have time to thoroughly review this right now, but please pester me during 0.16 to make sure this gets merged. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems really useful! I have some nits and suggestions, but nothing that should block it.
crates/bevy_ecs/src/component.rs
Outdated
) { | ||
} | ||
|
||
/// Wrapper for components clone specialization using autoderef |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, this is really clever!
This won't work for conditional impl
s, right? Like, #[derive(Component, Clone)] struct Foo<T>{ ... }
won't use component_clone_via_clone
even when T: Clone
. It might be worth noting that in a doc comment somewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true! Didn't think about that because generic components in general are a bit of a pain to work with. Not really sure where to add a note about that though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe there should be a section in the doc comments for EntityCloneBuilder
that explain the strategy for choosing the clone handler. There are actually quite a few ways it can be set: EntityCloneBuilder::override_component_clone_handler()
, Component::get_component_clone_handler()
, ComponentCloneHandlers::set_component_handler()
, ComponentCloneHandlers::set_default_handler()
, and the bevy_reflect
feature flag. And the docs for those methods could link back to that section so you can see their context.
pub struct EntityCloner<'a> { | ||
source: Entity, | ||
target: Entity, | ||
allowed: bool, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The allowed
field could use a doc comment, since the meaning isn't obvious from its name. It determines the meaning of filter
, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, will probably change it to filter_allows_components
, I've gotten confused about what allowed
means multiple times already.
.collect::<Vec<_>>(); | ||
|
||
for component in components { | ||
if !self.is_cloning_allowed(&component) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you do this with a filter()
call before collect()
so that you don't need to allocate space in the Vec
for components that will be skipped?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, will change.
.clone_handlers_overrides | ||
.is_handler_registered(component) | ||
{ | ||
self.clone_handlers_overrides.get_handler(component) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The double lookup here seems suboptimal. Could there be another version of get_handler
that returns an Option
so this can be a single lookup followed by an unwrap_or_else()
?
For that matter, why is clone_handlers_overrides
a ComponentCloneHandlers
instead of a plain HashMap<ComponentId, ComponentCloneFn>
? That adds some complexity, and you don't use the default_handler
field. I think it also means that override_component_clone_handler(id, ComponentCloneHandler::Default)
removes any overrides instead of overriding a custom handler with the default one, which seems surprising.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to reuse the interface of ComponentCloneHandlers
and had some plans to use default_handler
to replace filter entirely, but this is not true anymore. Agreed, should probably replace it with something simpler.
} | ||
|
||
/// Returns the current source entity. | ||
pub fn get_source(&self) -> Entity { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be more idiomatic to name these fn source()
and fn target()
without the get_
prefix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it seems that way, will change.
@@ -1834,3 +1926,106 @@ impl RequiredComponents { | |||
} | |||
} | |||
} | |||
|
|||
/// Component [clone handler function](ComponentCloneFn) implemented using the [`Clone`] trait. | |||
/// Can be [set](ComponentCloneHandlers::set_component_handler) as clone handler for the specific component it is implemented for. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to have a helper method that calls set_component_handler(id, component_clone_via_clone::<C>)
with the correct id
? You couldn't put it on ComponentCloneHandlers
since you can't get the component_id
from that, but you could put it on World
or Components
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how useful that will be. I though that most users would not need to add component_clone_via_clone
handlers at runtime, instead they would manually impl Clone
for a component or set a handler in get_component_clone_handler
. Is it for components with conditional Clone
implementation with generics?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was thinking about the conditional Clone
implementations. One of the reasons it seems perfectly fine to use default cloning in that case is that anyone who really needs component_clone_via_clone
can just register it for the concrete component types! But I agree that's likely to be rare, so it's probably not worth adding extra methods for it unless we see folks doing that in the wild.
|
||
/// Extend the list of components to clone. | ||
/// Calling this function automatically disallows all other components, only explicitly allowed ones will be cloned. | ||
pub fn allow_by_ids(&mut self, ids: impl IntoIterator<Item = TypeId>) -> &mut Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to have a way to use ComponentId
instead of TypeId
to support dynamic components that don't have TypeId
s. Methods like that are usually named _id
, so that's what I thought these were!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For that matter... why store TypeId
instead of ComponentId
? If you store the &mut World
in the EntityCloneBuilder
instead of taking it as a parameter to clone_entity
, then you can convert from TypeId
to ComponentId
immediately. That's what QueryBuilder
does, for example. That would also let you handle bundles without needing to store a fn
pointer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think this is a leftover from the previous design. Storing ComponentId
s makes more sense.
} | ||
|
||
/// Add a component to the list of components to clone. | ||
/// Calling this function automatically disallows all other components, only explicitly allowed ones will be cloned. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these methods explicitly clear the opposite list? I think cloner.deny::<A>().allow::<B>().deny::<C>()
will deny "A", even though the allow
method in the middle claims to have disallowed it.
Alternately, it might be more clear to require deny_all()
to be called, and then add or remove from the sets as needed, so cloner.deny_all().allow::<A>().allow::<B>().deny::<A>()
would re-deny A
while still allowing B
.
Not that anyone is likely to write either of those things...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reworked filtering using the alternative approach proposed.
|
||
/// Add a bundle of components to the list of components to clone. | ||
/// Calling this function automatically disallows all other components, only explicitly allowed ones will be cloned. | ||
pub fn allow_bundle<T: Bundle>(&mut self) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a blanket impl <C: Component> Bundle for C
, so you could combine this method with allow<T: Component>()
and just have a single allow()
and deny()
that works for any bundle, including plain components.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, I just though that storing a closure instead of an id just to allow a single component might not be optimal, but if builder is changed to store ComponentId
s anyway using World
that should be fine.
I did some basic profiling to see how cloning compares to just spawning an entity and performance is not that great (all time is relative to total runtime, as reported by
I think optimizations for |
Oh, sorry, I bet this was due to my suggestion to use
I'm not a maintainer, but I vote for merging this first and doing performance improvements as a follow-up, especially if you don't expect the perf improvements to change the public API. That lets it get used sooner in places where the current perf is already good enough! |
I think from the current public API only custom clone handlers ( Thinking about it a bit more though, maybe the current flexible API can co-exist with the new more optimized but limited API that will be implemented later. |
Objective
Fixes #1515
This PR implements a flexible entity cloning system. The primary use case for it is to clone dynamically-generated entities.
Example:
Solution
Commands
Add a
clone_entity
command to create a clone of an entity with all components that can be cloned. Components that can't be cloned will be ignored.If there is a need to configure the cloning process (like set to clone recursively), there is a second command:
Both of these commands return
EntityCommands
of the cloned entity, so the copy can be modified afterwards.Builder
All these commands use
EntityCloneBuilder
internally. If there is a need to clone an entity usingWorld
instead, it is also possible:Builder has methods to
allow
ordeny
certain components during cloning if required and can be extended by implementing traits on it. This PR includes two:CloneEntityWithObserversExt
to configure adding cloned entity to observers of the original entity, andCloneEntityRecursiveExt
to configure cloning an entity recursively.Clone implementations
By default, all components that implement either
Clone
orReflect
will be cloned (withClone
-based implementation preferred in case component implements both).This can be overriden on a per-component basis:
ComponentCloneHandlers
(component clone handler registry)Clone implementation specified in
get_component_clone_handler
will get registered inComponentCloneHandlers
(stored inbevy_ecs::component::Components
) at component registration time.The clone handler implementation provided by a component can be overriden after registration like so:
The default clone handler for all components that do not explicitly define one (or don't derive
Component
) iscomponent_clone_via_reflect
ifbevy_reflect
feature is enabled, andcomponent_clone_ignore
(noop) otherwise.Default handler can be overriden using
ComponentCloneHandlers::set_default_handler
Handlers
Component clone handlers can be used to modify component cloning behavior. The general signature for a handler that can be used in
ComponentCloneHandler::Custom
is as follows:The
EntityCloner
implementation (used internally byEntityCloneBuilder
) assumes that after calling this custom handler, thetarget
entity has the desired version of the component from thesource
entity.Builder handler overrides
Besides component-defined and world-overriden handlers,
EntityCloneBuilder
also has a way to override handlers locally. It is mainly used to allow configuration methods likerecursive
andadd_observers
.Testing
Includes some basic functionality tests, but haven't tested it on any real-world projects yet.
Also it would make sense to do some performance testing, I haven't done any yet.
Reflection-based component code is not as optimized as
Clone
-based approach. It could probably be optimized by adding someunsafe
, but I'm not comfortable enough with that yet and in generalClone
-based approach would theoretically still be faster.