-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Stop inserting RelationshipTarget
on SpawnRelatedBundle
#19726
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
base: main
Are you sure you want to change the base?
Stop inserting RelationshipTarget
on SpawnRelatedBundle
#19726
Conversation
46e6155
to
f2d4dc3
Compare
As a more hacky but less complex alternative, we could have |
I had arrived to a similar idea without the auto-removal, making that a sparse component would then make a lot of sense. Feels incredibly hacky but could make sense if the performance gains are good enough. |
I have had to change the |
Put this in 0.16.2 because even though it changes expected behavior to something else, I've seen quite a few people get tripped up by this, including me. |
What about something like: Children::spawn_append(...) And an associated This makes it explicit as to whether the children will be overwritten or appended. |
I am definitely not comfortable shipping this level of subtle behavior change in a point release. @Pascualex, I am going to request a migration guide here. |
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 needs a dedicated test, a migration guide and a fresh set of benchmarks. This is very performance sensitive code, and I'm skeptical that this is the right fix.
To me, the correct fix is "if a matching relationship target already exists, we should be appending to it", but that doesn't appear to be what these code changes are doing.
The allocation optimization was definitely a primary motivator. But additionally, from a "bundle principles" perspective, one thing I'm trying to uphold is "something in component position is that component". Part of that was making RelationshipTargets like Children actually insert that component on insert. If you were inserting any other component in that way, it would be "overwrite" behavior. I think preserving the "things in component position are components" principle is important for making Bevy code readable and predictable. I've been thinking about a related idea recently: allowing components (when implementing the Component trait) to define the "insert" behavior (replace vs merge ... defaulting to replace of course). In that world, Children could define an "append" merge behavior, allowing us to align world.spawn(
Foo,
children![A, B],
Bar,
// this would get merged with [A, B], producing [A, B, C]
children![C],
) That being said, I'm not fully sold on this design. It makes Bevy less predictable, as you then need to know if a component has merge inserts enabled (and you need to know what merge algorithm would be used). I'm also not convinced that allowing multiple |
EIBTI ("Explicit is Better than Implicit", from the Zen of Python) |
Would |
@alice-i-cecile that's fair! Seems like we need some alignment before committing to an implementation anyway so it will take some time.
It kind of does that in the sense that it delegates the management of the relationship target to the relationship component hooks, which behave that way. But yeah, it doesn't do that in advance to optimize performance.
@cart good to know, that changes things for me.
Hmm, I understand the concern and agree with the idea behind it. I think the main sauce of A related thought: the fact that currently if you use
Oh I see, that could even preserve the space-allocation optimization, I like it.
What if So we would replace the following: // old
commands.entity(entity).insert_if_new(ComponentA);
// new
commands.entity(entity).insert(Keep(ComponentA)); And similarly we could do: commands.entity(entity).insert(Merge(SpawnRelatedBundle { ... })); That way // old
commands
.entity(entity)
.insert(ComponentA)
.insert_if_new(ComponentB);
// new
commands
.entity(entity)
.insert((
ComponentA,
Keep(ComponentB),
)); Note that since it's a bundle wrapper it wouldn't introduce much complexity when applying to multiple components: // old
commands
.entity(entity)
.insert_if_new((
ComponentA,
ComponentB,
));
// new
commands
.entity(entity)
.insert(Keep((
ComponentA,
ComponentB,
))); Alternative names for
|
I've created #19768 prototyping the alternative proposal. |
Fixes #19715.
How the issue was introduced
On v0.16 improved spawn APIs were introduced. They are presented as having vastly improved ergonomics and the following comparison is offered as an example:
So both APIs do more or less the same thing but one is way more ergonomic than the other, right? Well, not exactly. As written on the release notes:
Unfortunately, this reasonable optimization requires a significant change in behavior: the
children
macro doesn't just spawn children (likewith_children
does) , it also inserts a newChildren
component beforehand.This introduces a footgun, as users may expect to be able to bring these ergonomic improvements to a slightly different context:
But here the behavior is widely different. Since the
children
macro inserts aChildren
component, this means that existing children will be overriden.Note that you can avoid the footgun by using
insert_if_new
, but it's very easy to forget and could cause issues for other components in the bundle:Ergonomics should be the focus
Although optimizations are always welcome, I believe that in this case the main goal of the new APIs was to improve ergonomics. I think we should drop the optimization in favor of removing a footgun that worsens ergonomics. To do that, we just need to stop inserting the
RelationshipTarget
component onSpawnRelatedBundle
.Alternatives
An alternative proposed by @SkiFire13 is to make use of required components to insert the
Children
component only when it doesn't exist. This wouldn't preallocate space like the current optimization, but that is probably less impactful than avoiding the table move. Although I find this a great optimization without footgun issues, establishing a requirement relationship betweenSpawnRelatedBundle
andRelationshipTarget
would likely require complex changes sinceSpawnRelatedBundle
is not a component.