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

Improved Entity Mapping and Cloning #17687

Merged
merged 14 commits into from
Feb 6, 2025
Merged

Conversation

cart
Copy link
Member

@cart cart commented Feb 5, 2025

Fixes #17535

Bevy's approach to handling "entity mapping" during spawning and cloning needs some work. The addition of Relations both introduced a new "duplicate entities" bug when spawning scenes in the scene system and made the weaknesses of the current mapping system exceedingly clear:

  1. Entity mapping requires a ton of boilerplate (implement or derive VisitEntities and VisitEntitesMut, then register / reflect MapEntities). Knowing the incantation is challenging and if you forget to do it in part or in whole, spawning subtly breaks.
  2. Entity mapping a spawned component in scenes incurs unnecessary overhead: look up ReflectMapEntities, create a brand new temporary instance of the component using FromReflect, map the entities in that instance, and then apply that on top of the actual component using reflection. We can do much better.

Additionally, while our new Entity cloning system is already pretty great, it has some areas we can make better:

  • It doesn't expose semantic info about the clone (ex: ignore or "clone empty"), meaning we can't key off of that in places where it would be useful, such as scene spawning. Rather than duplicating this info across contexts, I think it makes more sense to add that info to the clone system, especially given that we'd like to use cloning code in some of our spawning scenarios.
  • EntityCloner is currently built in a way that prioritizes a single entity clone
  • EntityCloner's recursive cloning is built to be done "inside out" in a parallel context (queue commands that each have a clone of EntityCloner). By making EntityCloner the orchestrator of the clone we can remove internal arcs, improve the clarity of the code, make EntityCloner mutable again, and simplify the builder code.
  • EntityCloner does not currently take into account entity mapping. This is necessary to do true "bullet proof" cloning, would allow us to unify the per-component scene spawning and cloning UX, and ultimately would allow us to use EntityCloner in place of raw reflection for scenes like Scene(World) (which would give us a nice performance boost: fewer archetype moves, less reflection overhead).

Solution

Improved Entity Mapping

First, components now have first-class "entity visiting and mapping" behavior:

#[derive(Component, Reflect)]
#[reflect(Component)]
struct Inventory {
    size: usize,
    #[entities]
    items: Vec<Entity>,
}

Any field with the #[entities] annotation will be viewable and mappable when cloning and spawning scenes.

Compare that to what was required before!

#[derive(Component, Reflect, VisitEntities, VisitEntitiesMut)]
#[reflect(Component, MapEntities)]
struct Inventory {
    #[visit_entities(ignore)]
    size: usize,
    items: Vec<Entity>,
}

Additionally, for relationships #[entities] is implied, meaning this "just works" in scenes and cloning:

#[derive(Component, Reflect)]
#[relationship(relationship_target = Children)]
#[reflect(Component)]
struct ChildOf(pub Entity);

Note that Component does not implement VisitEntities directly. Instead, it has Component::visit_entities and Component::visit_entities_mut methods. This is for a few reasons:

  1. We cannot implement VisitEntities for C: Component because that would conflict with our impl of VisitEntities for anything that implements IntoIterator<Item=Entity>. Preserving that impl is more important from a UX perspective.
  2. We should not implement Component: VisitEntities VisitEntities in the Component derive, as that would increase the burden of manual Component trait implementors.
  3. Making VisitEntitiesMut directly callable for components would make it easy to invalidate invariants defined by a component author. By putting it in the Component impl, we can make it harder to call naturally / unavailable to autocomplete using fn visit_entities_mut(this: &mut Self, ...).

ReflectComponent::apply_or_insert is now ReflectComponent::apply_or_insert_mapped. By moving mapping inside this impl, we remove the need to go through the reflection system to do entity mapping, meaning we no longer need to create a clone of the target component, map the entities in that component, and patch those values on top. This will make spawning mapped entities much faster (The default Component::visit_entities_mut impl is an inlined empty function, so it will incur no overhead for unmapped entities).

The Bug Fix

To solve #17535, spawning code now skips entities with the new ComponentCloneBehavior::Ignore and ComponentCloneBehavior::RelationshipTarget variants (note RelationshipTarget is a temporary "workaround" variant that allows scenes to skip these components. This is a temporary workaround that can be removed as these cases should really be using EntityCloner logic, which should be done in a followup PR. When that is done, ComponentCloneBehavior::RelationshipTarget can be merged into the normal ComponentCloneBehavior::Custom).

Improved Cloning

  • Option<ComponentCloneHandler> has been replaced by ComponentCloneBehavior, which encodes additional intent and context (ex: Default, Ignore, Custom, RelationshipTarget (this last one is temporary)).
  • Global per-world entity cloning configuration has been removed. This felt overly complicated, increased our API surface, and felt too generic. Each clone context can have different requirements (ex: what a user wants in a specific system, what a scene spawner wants, etc). I'd prefer to see how far context-specific EntityCloners get us first.
  • EntityCloner's internals have been reworked to remove Arcs and make it mutable.
  • EntityCloner is now directly stored on EntityClonerBuilder, simplifying the code somewhat
  • EntityCloner's "bundle scratch" pattern has been moved into the new BundleScratch type, improving its usability and making it usable in other contexts (such as future cross-world cloning code). Currently this is still private, but with some higher level safe APIs it could be used externally for making dynamic bundles
  • EntityCloner's recursive cloning behavior has been "externalized". It is now responsible for orchestrating recursive clones, meaning it no longer needs to be sharable/clone-able across threads / read-only.
  • EntityCloner now does entity mapping during clones, like scenes do. This gives behavior parity and also makes it more generically useful.
  • RelatonshipTarget::RECURSIVE_SPAWN is now RelationshipTarget::LINKED_SPAWN, and this field is used when cloning relationship targets to determine if cloning should happen recursively. The new LINKED_SPAWN term was picked to make it more generically applicable across spawning and cloning scenarios.

Next Steps

  • I think we should adapt EntityCloner to support cross world cloning. I think this PR helps set the stage for that by making the internals slightly more generalized. We could have a CrossWorldEntityCloner that reuses a lot of this infrastructure.
  • Once we support cross world cloning, we should use EntityCloner to spawn Scene(World) scenes. This would yield significant performance benefits (no archetype moves, less reflection overhead).

@cart cart added A-ECS Entities, components, systems, and events A-Scenes Serialized ECS data stored on the disk C-Usability A targeted quality-of-life change that makes Bevy easier to use C-Bug An unexpected or incorrect behavior labels Feb 5, 2025
@cart cart added this to the 0.16 milestone Feb 5, 2025
@cart cart force-pushed the improved-clone-and-map branch from c700dcf to 2f6a747 Compare February 5, 2025 02:33
@bushrat011899
Copy link
Contributor

Wasm CI issues should be resolved once you upstream from main.

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.

Mostly looked at the cloning changes here since this is the part I'm most familiar with.

@alice-i-cecile alice-i-cecile added S-Needs-Review Needs reviewer attention (from anyone!) to move forward P-High This is particularly urgent, and deserves immediate attention labels Feb 5, 2025
@cart cart force-pushed the improved-clone-and-map branch from 7bfe05a to e3ad84b Compare February 5, 2025 22:15
@alice-i-cecile alice-i-cecile added the M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide label Feb 6, 2025
Copy link
Contributor

github-actions bot commented Feb 6, 2025

It looks like your PR is a breaking change, but you didn't provide a migration guide.

Could you add some context on what users should update when this change get released in a new version of Bevy?
It will be used to help writing the migration guide for the version. Putting it after a ## Migration Guide will help it get automatically picked up by our tooling.

@alice-i-cecile alice-i-cecile added the A-Networking Sending data between clients, servers and machines label Feb 6, 2025
@alice-i-cecile
Copy link
Member

The entity mapping changes will have implications for our networking ecosystem too. Hopefully positive! I've pulled in a few reviewers from there to get their opinions.

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.

Alright, not as bad to review as I was worried about. I have a few cleanup nits, but the general strategy and motivation is sound, and I really like making the clone + despawn behavior a trait constant that we use for both behaviors.

Making sure that VisitEntities mentions and explains this new behavior is the main thing I want to see improved before this is merged.

Removing Arcs in EntityCloner makes me particularly happy; I'm glad you found a way to do that. Eliminating global cloning config also seems reasonable; I agree with your argument that this decision is extremly contextual.

This easy path for entity mapping will further widen the gap between components and resources, but as laid out in #17485, I don't think further duplicating features like this is sustainable.

I like both of your follow-up suggestions, but unsuprisingly I'd prefer they be done in later PRs :)

/// This will also queue up clones of the relationship sources if the [`EntityCloner`](crate::entity::EntityCloner) is configured
/// to spawn recursively.
pub fn clone_relationship_target<T: RelationshipTarget>(
_commands: &mut Commands,
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand why _commands exists here if this is unused.

Copy link
Member Author

Choose a reason for hiding this comment

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

This function must follow the ComponentCloneFn signature.

cart and others added 2 commits February 6, 2025 13:43
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
@cart cart added this pull request to the merge queue Feb 6, 2025
Merged via the queue into bevyengine:main with commit 3c8fae2 Feb 6, 2025
28 checks passed
mrchantey pushed a commit to mrchantey/bevy that referenced this pull request Feb 17, 2025
Fixes bevyengine#17535

Bevy's approach to handling "entity mapping" during spawning and cloning
needs some work. The addition of
[Relations](bevyengine#17398) both
[introduced a new "duplicate entities" bug when spawning scenes in the
scene system](bevyengine#17535) and made the weaknesses of the current mapping
system exceedingly clear:

1. Entity mapping requires _a ton_ of boilerplate (implement or derive
VisitEntities and VisitEntitesMut, then register / reflect MapEntities).
Knowing the incantation is challenging and if you forget to do it in
part or in whole, spawning subtly breaks.
2. Entity mapping a spawned component in scenes incurs unnecessary
overhead: look up ReflectMapEntities, create a _brand new temporary
instance_ of the component using FromReflect, map the entities in that
instance, and then apply that on top of the actual component using
reflection. We can do much better.

Additionally, while our new [Entity cloning
system](bevyengine#16132) is already pretty
great, it has some areas we can make better:

* It doesn't expose semantic info about the clone (ex: ignore or "clone
empty"), meaning we can't key off of that in places where it would be
useful, such as scene spawning. Rather than duplicating this info across
contexts, I think it makes more sense to add that info to the clone
system, especially given that we'd like to use cloning code in some of
our spawning scenarios.
* EntityCloner is currently built in a way that prioritizes a single
entity clone
* EntityCloner's recursive cloning is built to be done "inside out" in a
parallel context (queue commands that each have a clone of
EntityCloner). By making EntityCloner the orchestrator of the clone we
can remove internal arcs, improve the clarity of the code, make
EntityCloner mutable again, and simplify the builder code.
* EntityCloner does not currently take into account entity mapping. This
is necessary to do true "bullet proof" cloning, would allow us to unify
the per-component scene spawning and cloning UX, and ultimately would
allow us to use EntityCloner in place of raw reflection for scenes like
`Scene(World)` (which would give us a nice performance boost: fewer
archetype moves, less reflection overhead).

## Solution

### Improved Entity Mapping

First, components now have first-class "entity visiting and mapping"
behavior:

```rust
#[derive(Component, Reflect)]
#[reflect(Component)]
struct Inventory {
    size: usize,
    #[entities]
    items: Vec<Entity>,
}
```

Any field with the `#[entities]` annotation will be viewable and
mappable when cloning and spawning scenes.

Compare that to what was required before!

```rust
#[derive(Component, Reflect, VisitEntities, VisitEntitiesMut)]
#[reflect(Component, MapEntities)]
struct Inventory {
    #[visit_entities(ignore)]
    size: usize,
    items: Vec<Entity>,
}
```

Additionally, for relationships `#[entities]` is implied, meaning this
"just works" in scenes and cloning:

```rust
#[derive(Component, Reflect)]
#[relationship(relationship_target = Children)]
#[reflect(Component)]
struct ChildOf(pub Entity);
```

Note that Component _does not_ implement `VisitEntities` directly.
Instead, it has `Component::visit_entities` and
`Component::visit_entities_mut` methods. This is for a few reasons:

1. We cannot implement `VisitEntities for C: Component` because that
would conflict with our impl of VisitEntities for anything that
implements `IntoIterator<Item=Entity>`. Preserving that impl is more
important from a UX perspective.
2. We should not implement `Component: VisitEntities` VisitEntities in
the Component derive, as that would increase the burden of manual
Component trait implementors.
3. Making VisitEntitiesMut directly callable for components would make
it easy to invalidate invariants defined by a component author. By
putting it in the `Component` impl, we can make it harder to call
naturally / unavailable to autocomplete using `fn
visit_entities_mut(this: &mut Self, ...)`.

`ReflectComponent::apply_or_insert` is now
`ReflectComponent::apply_or_insert_mapped`. By moving mapping inside
this impl, we remove the need to go through the reflection system to do
entity mapping, meaning we no longer need to create a clone of the
target component, map the entities in that component, and patch those
values on top. This will make spawning mapped entities _much_ faster
(The default `Component::visit_entities_mut` impl is an inlined empty
function, so it will incur no overhead for unmapped entities).

### The Bug Fix

To solve bevyengine#17535, spawning code now skips entities with the new
`ComponentCloneBehavior::Ignore` and
`ComponentCloneBehavior::RelationshipTarget` variants (note
RelationshipTarget is a temporary "workaround" variant that allows
scenes to skip these components. This is a temporary workaround that can
be removed as these cases should _really_ be using EntityCloner logic,
which should be done in a followup PR. When that is done,
`ComponentCloneBehavior::RelationshipTarget` can be merged into the
normal `ComponentCloneBehavior::Custom`).

### Improved Cloning

* `Option<ComponentCloneHandler>` has been replaced by
`ComponentCloneBehavior`, which encodes additional intent and context
(ex: `Default`, `Ignore`, `Custom`, `RelationshipTarget` (this last one
is temporary)).
* Global per-world entity cloning configuration has been removed. This
felt overly complicated, increased our API surface, and felt too
generic. Each clone context can have different requirements (ex: what a
user wants in a specific system, what a scene spawner wants, etc). I'd
prefer to see how far context-specific EntityCloners get us first.
* EntityCloner's internals have been reworked to remove Arcs and make it
mutable.
* EntityCloner is now directly stored on EntityClonerBuilder,
simplifying the code somewhat
* EntityCloner's "bundle scratch" pattern has been moved into the new
BundleScratch type, improving its usability and making it usable in
other contexts (such as future cross-world cloning code). Currently this
is still private, but with some higher level safe APIs it could be used
externally for making dynamic bundles
* EntityCloner's recursive cloning behavior has been "externalized". It
is now responsible for orchestrating recursive clones, meaning it no
longer needs to be sharable/clone-able across threads / read-only.
* EntityCloner now does entity mapping during clones, like scenes do.
This gives behavior parity and also makes it more generically useful.
* `RelatonshipTarget::RECURSIVE_SPAWN` is now
`RelationshipTarget::LINKED_SPAWN`, and this field is used when cloning
relationship targets to determine if cloning should happen recursively.
The new `LINKED_SPAWN` term was picked to make it more generically
applicable across spawning and cloning scenarios.

## Next Steps

* I think we should adapt EntityCloner to support cross world cloning. I
think this PR helps set the stage for that by making the internals
slightly more generalized. We could have a CrossWorldEntityCloner that
reuses a lot of this infrastructure.
* Once we support cross world cloning, we should use EntityCloner to
spawn `Scene(World)` scenes. This would yield significant performance
benefits (no archetype moves, less reflection overhead).

---------

Co-authored-by: eugineerd <70062110+eugineerd@users.noreply.github.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
@chescock chescock mentioned this pull request Feb 23, 2025
github-merge-queue bot pushed a commit that referenced this pull request Mar 21, 2025
# Objective

There are currently too many disparate ways to handle entity mapping,
especially after #17687. We now have MapEntities, VisitEntities,
VisitEntitiesMut, Component::visit_entities,
Component::visit_entities_mut.

Our only known use case at the moment for these is entity mapping. This
means we have significant consolidation potential.

Additionally, VisitEntitiesMut cannot be implemented for map-style
collections like HashSets, as you cant "just" mutate a `&mut Entity`.
Our current approach to Component mapping requires VisitEntitiesMut,
meaning this category of entity collection isn't mappable. `MapEntities`
is more generally applicable. Additionally, the _existence_ of the
blanket From impl on VisitEntitiesMut blocks us from implementing
MapEntities for HashSets (or any types we don't own), because the owner
could always add a conflicting impl in the future.

## Solution

Use `MapEntities` everywhere and remove all "visit entities" usages.

* Add `Component::map_entities`
* Remove `Component::visit_entities`, `Component::visit_entities_mut`,
`VisitEntities`, and `VisitEntitiesMut`
* Support deriving `Component::map_entities` in `#[derive(Coomponent)]`
* Add `#[derive(MapEntities)]`, and share logic with the
`Component::map_entities` derive.
* Add `ComponentCloneCtx::queue_deferred`, which is command-like logic
that runs immediately after normal clones. Reframe `FromWorld` fallback
logic in the "reflect clone" impl to use it. This cuts out a lot of
unnecessary work and I think justifies the existence of a pseudo-command
interface (given how niche, yet performance sensitive this is).

Note that we no longer auto-impl entity mapping for ` IntoIterator<Item
= &'a Entity>` types, as this would block our ability to implement cases
like `HashMap`. This means the onus is on us (or type authors) to add
explicit support for types that should be mappable.

Also note that the Component-related changes do not require a migration
guide as there hasn't been a release with them yet.

## Migration Guide

If you were previously implementing `VisitEntities` or
`VisitEntitiesMut` (likely via a derive), instead use `MapEntities`.
Those were almost certainly used in the context of Bevy Scenes or
reflection via `ReflectMapEntities`. If you have a case that uses
`VisitEntities` or `VisitEntitiesMut` directly, where `MapEntities` is
not a viable replacement, please let us know!

```rust
// before
#[derive(VisitEntities, VisitEntitiesMut)]
struct Inventory {
  items: Vec<Entity>,
  #[visit_entities(ignore)]
  label: String,
}

// after
#[derive(MapEntities)]
struct Inventory {
  #[entities]
  items: Vec<Entity>,
  label: String,
}
```
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 A-Networking Sending data between clients, servers and machines A-Scenes Serialized ECS data stored on the disk C-Bug An unexpected or incorrect behavior C-Usability A targeted quality-of-life change that makes Bevy easier to use M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide P-High This is particularly urgent, and deserves immediate attention S-Needs-Review Needs reviewer attention (from anyone!) to move forward
Projects
None yet
Development

Successfully merging this pull request may close these issues.

GLTF loading and animation performance regression
4 participants