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

Add methods to bulk replace relationships on a entity #18058

Merged
merged 10 commits into from
Mar 19, 2025

Conversation

Brezak
Copy link
Contributor

@Brezak Brezak commented Feb 26, 2025

Objective

Add a way to efficiently replace a set of specifically related entities with a new set.
Closes #18041

Solution

Add new replace_(related/children) to EntityWorldMut and friends.

Testing

Added a new test to hierarchy.rs that specifically check if replace_children actually correctly replaces the children on a entity while keeping the original one.


Showcase

EntityWorldMut and EntityCommands can now be used to efficiently replace the entities a entity is related to.

/// `parent` has 2 children. `entity_a` and `entity_b`.
assert_eq!([entity_a, entity_b], world.entity(parent).get::<Children>());

/// Replace `parent`s children with `entity_a` and `entity_c`
world.entity_mut(parent).replace_related(&[entity_a, entity_c]);

/// `parent` now has 2 children. `entity_a` and `entity_c`.
///
/// `replace_children` has saved time by not removing and reading
/// the relationship between `entity_a` and `parent`
assert_eq!([entity_a, entity_c], world.entity(parent).get::<Children>());

@alice-i-cecile alice-i-cecile added this to the 0.16 milestone Feb 27, 2025
@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events C-Performance A change motivated by improving speed, memory usage or compile times C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Feb 27, 2025
@Brezak Brezak force-pushed the replace-related branch 2 times, most recently from 6f549b7 to db43a02 Compare February 27, 2025 09:09
@Brezak Brezak requested a review from viridia February 27, 2025 09:15
Comment on lines 43 to 63
let mut potential_relations = EntityHashSet::from_iter(related.iter().copied());
let mut relations_to_remove = EntityHashSet::with_capacity(related.len());

for related in existing_relations.iter() {
if !potential_relations.remove(&related) {
relations_to_remove.insert(related);
}
}

let id = self.id();
self.world_scope(|world| {
for related in relations_to_remove {
world.entity_mut(related).remove::<R>();
}

for related in potential_relations {
world.entity_mut(related).insert(R::from(id));
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

A problem with this approach is that the resulting target collection may not result in the same order as the passed in related.

As a user of replace_related I would assume I can pass in a new order of the same set of entities to reorder them (for relationships requiring a stable ordering, e.g. Children).

I'm also curious about the performance benefits of this approach (allocating hashsets) over just removing the target collection and calling add_children (archetype moves?).

Copy link
Contributor

Choose a reason for hiding this comment

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

In the original issue I posted (#18041), I proposed a method to do this without using hash sets, however it entails a more complex API in which the caller does the work of determining which entities are added and removed. This puts the responsibility on the caller to ensure that the arguments are consistent.

In my various reactive experiments, I need to do this computation anyway (for example foreach computes the diff as part of its operation), so it's relatively easy for me to pass that information along to the replace_related API. Having replace_related do the calculation internally means that the set operations get done twice.

That being said, I may be overly/prematurely optimizing here. Maybe using hash sets is OK? I can't really say one way or the other.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe an optimized replace could benefit from the new RelationshipInsertHookMode in #17858

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 not too fond of requiring the user to pass correct lists. Seems like this can easily be misunderstood/lead to bugs.

For more specialized scenarios (like tree rendering mechanisms) where the caller already knows the “diff”, there could be an alternative function taking the new_related and deleted with a name that communicates the “riskiness”.

Or those use cases can update the relationship components directly, and use the hook mode to avoid archetype moves.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A problem with this approach is that the resulting target collection may not result in the same order as the passed in related.

We could directly modify the target collection to change the ordering but that would conflict with users overriding relationship on_insert hooks. My opinion is that a user will break any assumptions we make if given the chance. Imagine for example a on_insert hook that filters what entities can be inserted into a relationship. If we then manually insert entities into this collection we would be bypassing this filtering.

For more specialized scenarios (like tree rendering mechanisms) where the caller already knows the “diff”, there could be an alternative function taking the new_related and deleted with a name that communicates the “riskiness”.

I'd be willing to add this method too, but such a method requires #17858. Otherwise the method would be left with just calculating a difference between the insert and remove entity sets and inserting that (if we add any existing relationships the on_add hook will cause a double insert). Exactly what the current implementation does (and calculating a difference either requires a allocated set or the ability to sort the passed in slices. i.e. either allocating or breaking ordering).

Copy link
Member

Choose a reason for hiding this comment

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

#17858 has been merged now :)

github-merge-queue bot pushed a commit that referenced this pull request Mar 10, 2025
…18241)

# Objective

While redoing #18058 I needed `RelationshipSourceCollection` (henceforth
referred to as the **Trait**) to implement `clear` so I added it.

## Solution

Add the `clear` method to the **Trait**.
Also make `add` and `remove` report if they succeeded.

## Testing

Eyeballs

---

## Showcase

The `RelationshipSourceCollection` trait now reports if adding or
removing an entity from it was successful.
It also not contains the `clear` method so you can easily clear the
collection in generic contexts.

## Changes

EDITED by Alice: We should get this into 0.16, so no migration guide
needed.

The `RelationshipSourceCollection` methods `add` and `remove` now need
to return a boolean indicating if they were successful (adding a entity
to a set that already contains it counts as failure). Additionally the
`clear` method has been added to support clearing the collection in
generic contexts.
@Brezak Brezak force-pushed the replace-related branch 2 times, most recently from 09c7fd7 to e9eeb9f Compare March 12, 2025 22:07
Copy link
Contributor

@viridia viridia 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. This will be a fundamental building block for entity templating systems.

@Brezak
Copy link
Contributor Author

Brezak commented Mar 12, 2025

This is mostly done. The documentation needs some editing and the naming could use some bikeshedding. Sadly I'm spent for today. See you tomorrow.

@Brezak Brezak requested review from alice-i-cecile and villor March 12, 2025 22:46
Copy link
Contributor

@villor villor left a comment

Choose a reason for hiding this comment

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

Nice job on the optimized version! 👍 Really liking the debug_assertions too.

Still have some concerns about the ordering for replace_related. And some minor perf comments etc.

@Brezak
Copy link
Contributor Author

Brezak commented Mar 13, 2025

This should be done now. There's still some performance left on the table (see #18300) but otherwise everything works and tests pass.

One thing we should do is write down all the assumptions we make about relationship hooks. They're currently publicly accessible and a motivated user could override them and make them do some wild stuff. Alternatively we could add #[doc(hidden)] to them.

@Brezak Brezak requested a review from villor March 13, 2025 22:20
Copy link
Contributor

@villor villor left a comment

Choose a reason for hiding this comment

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

LGTM 👍

@villor
Copy link
Contributor

villor commented Mar 13, 2025

One thing we should do is write down all the assumptions we make about relationship hooks. They're currently publicly accessible and a motivated user could override them and make them do some wild stuff. Alternatively we could add #[doc(hidden)] to them.

I don't think hooks are overridable. And the derive macro stops you from adding hooks that are already used for relationship components.

@Brezak
Copy link
Contributor Author

Brezak commented Mar 14, 2025

I don't think hooks are overridable. And the derive macro stops you from adding hooks that are already used for relationship components.

We allow users to impl Component without the macro. We even have an example doing just that. And if a user is doing that they would also need to manually implement the Relationship trait at which point they can override whatever they want.

@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Mar 18, 2025
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.

Sensible and well-tested. I've fixed a grammar issue, but now I'm comfortable merging this.

auto-merge was automatically disabled March 18, 2025 22:22

Head branch was pushed to by a user without write access

bevyengine#18378 renamed `RelationshipInsertHookMode`
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Mar 19, 2025
Merged via the queue into bevyengine:main with commit 28907ae Mar 19, 2025
32 checks passed
mockersf pushed a commit that referenced this pull request Mar 19, 2025
# Objective

Add a way to efficiently replace a set of specifically related entities
with a new set.
Closes #18041 

## Solution

Add new `replace_(related/children)` to `EntityWorldMut` and friends.

## Testing

Added a new test to `hierarchy.rs` that specifically check if
`replace_children` actually correctly replaces the children on a entity
while keeping the original one.

---

## Showcase

`EntityWorldMut` and `EntityCommands` can now be used to efficiently
replace the entities a entity is related to.

```rust
/// `parent` has 2 children. `entity_a` and `entity_b`.
assert_eq!([entity_a, entity_b], world.entity(parent).get::<Children>());

/// Replace `parent`s children with `entity_a` and `entity_c`
world.entity_mut(parent).replace_related(&[entity_a, entity_c]);

/// `parent` now has 2 children. `entity_a` and `entity_c`.
///
/// `replace_children` has saved time by not removing and reading
/// the relationship between `entity_a` and `parent`
assert_eq!([entity_a, entity_c], world.entity(parent).get::<Children>());

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
github-merge-queue bot pushed a commit that referenced this pull request Mar 20, 2025
# Objective

While working on #18058 I realized I could use
`RelationshipTargetCollection::new`, so I added it.

## Solution

- Add `RelationshipTargetCollection::new`
- Add `RelationshipTargetCollection::reserve`. Could generally be useful
when doing micro-optimizations.
- Add `RelationshipTargetCollection::shrink_to_fit`. Rust collections
generally don't shrink when removing elements. Might be a good idea to
call this once in a while.

## Testing

`cargo clippy`

---

## Showcase

`RelationshipSourceCollection` now implements `new`, `reserve` and
`shrink_to_fit` to give greater control over how much memory it
consumes.

## Migration Guide

Any type implementing `RelationshipSourceCollection` now needs to also
implement `new`, `reserve` and `shrink_to_fit`. `reserve` and
`shrink_to_fit` can be made no-ops if they conceptually mean nothing to
a collection.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com>
@Brezak Brezak deleted the replace-related branch March 21, 2025 16:41
mockersf pushed a commit that referenced this pull request Mar 23, 2025
# Objective

While working on #18058 I realized I could use
`RelationshipTargetCollection::new`, so I added it.

## Solution

- Add `RelationshipTargetCollection::new`
- Add `RelationshipTargetCollection::reserve`. Could generally be useful
when doing micro-optimizations.
- Add `RelationshipTargetCollection::shrink_to_fit`. Rust collections
generally don't shrink when removing elements. Might be a good idea to
call this once in a while.

## Testing

`cargo clippy`

---

## Showcase

`RelationshipSourceCollection` now implements `new`, `reserve` and
`shrink_to_fit` to give greater control over how much memory it
consumes.

## Migration Guide

Any type implementing `RelationshipSourceCollection` now needs to also
implement `new`, `reserve` and `shrink_to_fit`. `reserve` and
`shrink_to_fit` can be made no-ops if they conceptually mean nothing to
a collection.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com>
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-Performance A change motivated by improving speed, memory usage or compile times C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bulk operations on entity relationships
4 participants