-
-
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
Support Removing Required Components at Runtime #14927
Comments
Great writeup, and I think this is an essential escape hatch. I think that the relatively straightforward "just register / unregister required components" is the right call for now. |
Since @alice-i-cecile asked me to put my more detailed description of Components can register theirselves as providing another component. If we want to add Some interesting usecases for this would be replacing things like This feature could of course be extended in many ways (like making things mutually exclusive if we ever get archetype invariants), but for this usecase having it as a way to avoid required components inserting unwanted components would be enough. (This idea was originally brought up by @NthTensor, which in turn was inspired by another idea from Joy relating to plugin dependencies) |
Long-term, I think we're going to want both: |
In general I think this is a pattern we should not be encouraging. If some third party A developer choosing to break that assumption risks breaking third party code in unexpected and arbitrary ways. And as these dependencies update, they risk adding new cases that also break. Encouraging the "remove arbitrary upstream components" pattern is abstraction-breaking and "ecosystem stability"-hostile. The ability to remove a required component constraint is an "unsafe escape hatch". If we were to include it, I think we'd want to heavily discourage its use in 3rd party plugins. I also think we should heavily reconsider adding a global #[derive(Component)]
#[require(SpecialComponent)]
#[remove_require(SpecialComponent, Transform)]
struct MySpecialComponent; This would mean that existing uses of SpecialComponent do not break. This comes at the cost of needing to specify
In the context of a #[derive(Component)]
#[require(Transform)]
struct Position {
x: f32,
y: f32,
z: f32,
}
#[derive(Component, Default)]
#[require(Transform)]
struct Rotation(Quat);
#[derive(Component)]
#[require(Transform)]
struct MyThing;
commands.spawn((
MyThing,
Position { x: 1.0, y: 0.0, z: 0.0 },
Rotation::default(),
));
fn sync_position_rotation(query: Query<(&Position, &Rotation, &mut Transform)>) {
// sync code here
} |
For more context,
It may be that the best solution here is to close this as Won't Fix (following Cart's points about abstraction breaking), but work harder to make sure that Transform can meet the needs of all of our users by solving some of these problems (or at least making more behavior configurable). |
This is the general idea of Provides, which seems really different from your example. #[derive(Component)]
struct A;
#[derive(Component)]
#[provides(A)]
struct B;
#[derive(Component)]
#[requires(A)]
struct C;
commands.spawn((B, C)); // does not insert A The idea is that inserting Without a mechanism like this it will quite difficult to alter the existing semantics of a component imported from a dependency. That dosn't seem very "Modular: Use only what you need. Replace what you don't like" to me. |
The only way I can see this working in a way that doesn't break arbitrary upstream code is using dynamic dispatch, which for the case of per-frame-per-entity Transform code would not be ideal. |
And if the goal is to remove a stored computed |
Because now we can output multiple archetypes? I can see how that might be an issue.
The point is not to remove a stored computed Transform. The point is rather to, for example, allow users to replace the bevy built-in transform with their own Provides lets you say 'actually, I have my own |
Can you describe how this would be implemented functionally, if not in a way that:
I'm having trouble understanding what your ideal system is. |
Additionally, the "common interface" is going to need to select things like layout + precision, which will be opinionated in the same way that Transform is currently opinionated. |
We're not talking about having these share traits, or have one component type actually provide an interface for another. I do not know where you are getting "common interface" from. The rule, as I originally framed it, is as follows: if #[require(Transform)]
struct SpecialComponent;
#[require(SpecialComponent)]
#[remove_require(SpecialComponent, Transform)]
struct MySpecialComponent;
// vs
#[require(SpecialComponent)]
#[provides(Transform)]
struct MySpecialComponent; The last two defs do the same thing. |
I need time to digest Carts original response, but wanted to flag that this issue isn't intended to be about Transform replacement specifically, that was just the best thing I could come up with as an example for the original issue. I'd like people not to get too focussed on the specifics of Transforms. |
I think its good to explore this usecase because:
|
Gotcha. I was playing off of this "contract" phrasing:
In combination with stated desire to have something less flimsy than reaching in to replace a component with another component. I'm not really a fan of
I don't see it as meaningfully better than in-place |
Fair enough, I can agree with that. I do have thoughts about making transforms more modular, and you did ask some interesting questions I'd like to answer. But I view them as separate from this issue, and think they deserve a proper proposal to lay everything out. |
I see a lot of mentions of "breaking upstream assumptions", but I don't think that's fully accurate. There's plenty of cases where a component might be registered as required but not actually necessary, many bundles in bevy currently include I also think we should move away from insisting things (especially user code) shouldn't be able to break plugin assumptions, especially if those assumptions are completely unfounded because we do not in fact have anything like archetype invariants. I've seen people use the same argument to resist entity disabling, while in reality not having good solutions for such problems often causes far more problems than having them ever could. |
I also don't see how |
I have two problems with this assertion:
From my point of view, required components are a standardised replacement for systems which would add required components, which avoid common developer pitfalls and improve performance. As such, in the same way that one can remove a system, one should be able to remove a requirement.
I would argue that not including the escape hatch is a big risk to the ecosystem also. My personal experience with bevy is that I constantly have to fork crates because they haven't followed good practice in a way which allows me to prevent them from misbehaving.
This I broadly agree with - I would really only expect this to be used at the root application level. I fully expect it to be a common occurrence though. |
Notably, we can't currently remove systems (although you can permanently disable them), and doing so requires exposing a public API for the system: either a system set or a public fn type. Cart's been consistent with the stance against reaching in and messing with plugin internals over the years, so the pushback here doesn't surprise me. |
I can see the argument that if you don't want Both of these sides seem pretty reasonable to me, I'd be surprised if we can't come to some kind of common understanding here. |
Of course when bevy makes real guarantees to end users, with something like archetype invariants, we can't let other crates or end users mess with those, because unsafe code later might be built on those guarantees. It is however extremely unlikely we'd see something like "Every archetype with |
I haven't thought this through, but just to throw ideas at the wall: instead of adding and removing requirements, you could have variants of the |
I'd imagine this would be very much possible, but I doubt it would very practically solve this problem. And at the same time you'd have to manually list out all the other requires, which could get quite annoying. I imagine there's basically two cases for removing a require:
Meanwhile adding a require could be the counterpart of reason 2, looser coupling to optional plugins in the same crate (" |
FYI, there's a pretty ergonomic (if inefficient) workaround for this using observers: listen for whenever a component of the undesired type is added, and then replace it with your own. Definitely wasteful, but foolproof and very easy to set up. You can even check within that observer if the entity meets some other condition. |
From my point of view, the claimed pros/cons of required components are: Pros:
Cons:
I have concerns about the pros, though:
Since Required Components are going ahead regardless we're going to have to just get on with it and learn together how this will go. This issue request is effectively just suggesting a mitigation for a downside of this new feature. Got a bit over excited there, now having read Alice's comment: this means there is a known workaround to mitigate this. With that in place, it's now only really a question of what our stance is on the issue, this work isn't required because a workaround exists. This issue only really serves a purpose if we want a better workaround, or use this opportunity to brainstorm how we communicate the new required components feature. Are we going to encourage people to use it? For what? Are we going to discourage people from using it? Are we going to document the pros/cons of using it? |
I think the pros of required components are actually:
What it doesn't do is:
Alice's workaround would work, but it feels counter to advantage 2. The Also I think it's important to note that required components isn't a BSN requirement, BSN could probably work with bundles, it would just be clunky. The community however decided they'd rather see required components first |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
Yeah we've discussed only requiring GlobalTransform in these contexts. However in cases where users are expected to position entities, we do really want to include a fn x(query: Query<&mut Transform, With<X>>) {
} For a given
I think the solution is to prefer
Definitely true, although I'll still assert that removing a component like Transform is unadvisable generally, given the unpredictable upstream effects. This argument applies to all upstream components outside of the users control. Don't remove something unless it was designed to be removed.
Allowing people to build solid abstractions is a critical piece of the "modular ecosystem" puzzle. See my reply below for thoughts on this.
Requires are an actual, reliable invariant in that they guarantee that the components will exist at insertion time. Supporting Requires are not just a convenience from my perspective. They exist as a piece of the puzzle to build reliable abstractions. In this case, they exist to define a predictable / functional initial state of an entity.
The risk that providing an escape hatch poses is as high if not higher imo (see my next reply).
Being able to build private, unaddressable abstractions that downstream users cannot depend on is an important tool for enabling modularity actually. Making everything public/addressable/usable creates a world where people build on the details of an implementation that were not intentionally designed to be built on. An ecosystem where people are building on internal implementation details will chaotically break, making it hard to trust. An important part of making Bevy "modular" is intentional design of public APIs and intentional hiding of implementation details. |
I definitely agree that doing this arbitrarily would be unadvisable. This is however an API you have to go out of your way to use, and it could have plenty of warnings included so people understand that if things break it's their fault.
I guess that is indeed a guarantee, it is a bit limited in its scope but things like hooks and observers would still allow for this guarantee to be used. One possibility would be for "add this for convenience" and "add this because I need the guarantee" separate, but I'd imagine it would be hard to get right.
One option could be to have |
Not really an option on its own, as we're already living in a world where we don't want Transform for every GlobalTransform (ex: we're discussing Transform2d and removing Transform for UI Nodes) |
Yea I guess this specific case wouldn't work out once we split up |
I don't think I agree with this. To take it slightly ad absurdum, does this mean we should never destroy entities with components that shouldn't be removed (as destruction implies removal of all components)? Is this to protect against bugs in cleanup?
I absolutely agree with this, I think we're just considering the boundary of the abstractions to be in different places.
Is this a desirable invariant? Is it even observable? If you remove the component with a hook as soon as it is added, technically the invariant has been upheld (the component was added at insertion time), but no library author could rely on this for anything.
I don't think that requires are at the right level to be useful for building abstractions. Since the components on an entity are inherently publicly exposed, any abstraction over which components are associated with an entity is going to be leaky. There's no way for a library to maintain its invariants between calls. You can't have a functional initial state and multiple ways to structure an entity. If you have a
I think that it's already hard to reason about what needs to be exposed as part of the interface of a plugin crate. Systems being addressable (by system or exposing sets) so that ordering is part of the interface is a common oversight. At the moment the reality is that Bevy plugins aren't all successful abstractions. If they were, we wouldn't have the integration issues we do. If requires are going to be used to provide reasonable defaults, I definitely think that an escape hatch is justified (since that's well outside the remit of invariants), but even otherwise, I think it's far preferable to forking crates. |
Is it possible to get some form of clarity on this issue before the 0.15 RC? If I understand the current status-quo, once 0.15 is released, plugin authors will be encouraged to use requires, and in particular should be able to treat required component co-existence as invariant. This says to me that using Additionally, requires will be recommended for sensible initial state and plugin authors will have to choose between making their plugins more flexible, or easy to use. This situation makes me pretty uncomfortable. |
This is not the case: please don't rely on dependent components existing! These aren't archetype invariants: it's only a guarantee at insertion time, not for the whole lifetime. Your plugin code should still try to be reasonably robust to these failing, especially if you expose public systems or other backdoors. |
Alice already responded while I was writing my own message :p but leaving it here anyway:
Required components are primarily an insertion mechanism that ensures that components are initialized with all the other components that they need to function as expected. This can be thought of as a kind of invariant, but not one that is enforced at runtime. As a plugin author, I don't expect to be able to treat this as as an absolute, and to be able to use Also, this is already kind of the case with bundles. You could assume that users will always use As for one of the other things raised in this issue, adding requirements at runtime, that was addressed by #15458. It also makes it technically possible to have optional requirements if they are added through plugins (just don't add the plugin). Arbitrarily removing requirements on the other hand I consider to be a non-goal for several reasons already discussed here. |
I think we've exhausted this conversation. We can see how this plays out in practice and reopen this if there are ecosystem problems, but I'm going to mark "removing required component constraints" as won't fix. |
I'm not planning to do this, or recommending it, I was just trying to summarise what the current stance is (though it looks like I'm still missing something). To quote @cart:
This is what lead me to the conclusion I made above. The further comment about not removing components that are not designed to be removed seems in-line too. @Jondolf, I hope your interpretation is correct, but it seems to me that there's not a consensus in this particular issue. Anyway, my goal here wasn't to create more noise, as @alice-i-cecile says, we can see how this plays out. 🙂 |
I said this in the context of removing requires defined by components you depend on (not removing components brought in by a require). With requires, the one invariant you should be able to rely on is "this will exist at insertion time". All other constraints about what you can remove are defined by the context of each component. |
What problem does this solve or what need does it fill?
We are adding the ability to require components in Bevy, this feature request asks for the ability to override this behaviour when it prevents you from customizing third party code.
For example, suppose you're using some hypothetical Bevy provided components & systems:
Suppose there are components:
SpecialComponent
(which requiresTransform
),Transform
(which requiresGlobalTransform
), andGlobalTransform
.Suppose there is the system:
update_globaltransform_for_specialcomponents_with_transforms
.You're having a great time, until you reach an edge case where your requirements deviate from Bevy's assumptions, so you try to swap out some types:
You create a new component:
BetterTransform
.You create a new system:
update_globaltransform_for_specialcomponents_with_bettertransforms
.Pre-"Required Components": you would be unable to use bundles that assumed
SpecialComponent
should use Transform, potentially defining your own new bundles for convenience, but you'd be able to move on and achieve your goals.In a world with "Required Components": you cannot currently "un-require"
Transform
fromSpecialComponent
in 3rd party code (without forking), resulting in this situation:Your types end up with all of the components:
SpecialComponent
pulls inTransform
, you addBetterTransform
,Transform
andBetterTransform
pull inGlobalTransform
.You end up with two systems trying to compute the
GlobalTransform
forSpecialComponents
:update_globaltransform_for_specialcomponents_with_transforms
, andupdate_globaltransform_for_specialcomponents_with_bettertransforms
.What solution would you like?
Currently the required components are authored using an annotation. This feature request proposes that methods be added to Bevy's
App
type for customizing them dynamically, a hypothetical set of methods:app.add_requires::<DependantComponent, RequiredComponent>();
app.remove_requires::<DependantComponent, RequiredComponent>();
(Names bike-sheddable as always!).
These would, as the names hopefully suggest, dynamically (un)register the "required components" relationship between those two types. The annotations in the current implementation would just be a short-hand for automatically registering these relationships.
What alternative(s) have you considered?
Extensive discussion in Discord (the Next Generation Scene/UI Working Group channel), including but not limited to:
Effectively, Required Components are going in (soon? today? already?), so we're committed to experimenting with those and learning from it. This feature request effectively acts as a short-term solution to ensure the addition of Required Components doesn't cause a surge in use that prevents customizability.
Additional context
N/A
The text was updated successfully, but these errors were encountered: