-
Notifications
You must be signed in to change notification settings - Fork 352
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
0.15 Release Notes: Required Components #1779
Conversation
release-content/0.15/release-notes/14791_Required_Components.md
Outdated
Show resolved
Hide resolved
We're going to need to overhaul these if / when we merge bevyengine/bevy#16267 but we shouldn't let that block this PR. |
I think this would largely remain intact. Imo the other options should be an additional inclusion at the end, rather than a focal point introduced early. |
I'm nervous that if we make
|
My general thought is that this is (approximately) what we want, and that most of the value of the system (the "driver" paradigm being introduced) comes from focusing on this. Required Components are for defining "what" an entity is at its core. I think a strong focus on defining "fixed" reliable "types" of entities is important for the future of Bevy. It encourages everyone to build predictable, easy to understand structured APIs. Included components are for additional minor conveniences layered on top (where flexibility is needed). Fully optional included components will be a niche "break glass" concept used in cases where muddying the waters of "what" an entity is fundamentally is worth the flexibility benefits. Ex: in the context of all existing "required component" uses in upstream Bevy, I can't think of a single case that shouldn't be "required" right now, other than maybe Transform (but that specific case is complicated and controversial) |
Hmm. I think that "included components" should be the go-to tool by default, with required components being a special tool for important invariants (like the render world extraction stuff). To 99% of users the difference is immaterial, but I'm not sold that preventing people from e.g. writing their own Disabling (or even removing) an included component is very much outside of the class of "obvious mistakes" that I've seen users make, so I'm not sure what invariants we're protecting by suggesting that users (and Bevy) prefer required by default. If a user disables the required component between To be clear, I do really like the use of a "game object"-flavored API for Bevy (that can be composed of multiple entities) for tooling and pedagogical reasons. I'm just not quite piecing together how increased strictness helps us achieve those goals. |
The Schools of ThoughtI think "hard component requirements by default " is extremely important for Bevy's future. There are primarily two schools of thought in the Bevy community right now:
I am firmly in camp (2). Everyone in the Bevy ecosystem when implementing behaviors should be thinking in terms of either: a. How do I extend existing Component/Behavior context with additional functionality (ex: add new systems). People in camp (1) regularly want to perform arbitrary "mutations" (edits / deletes) of existing context (ex: Remove a required component. Insert a standalone component without other components that happen to produce the original intended behaviors, then add new behaviors / new context on top). I will assert that this is essentially never a good thing:
In both of these cases, doing something like stripping out a "required" component to remove the default behavior might accidentally work today. But tomorrow when the original author (still operating under the assumptions of their design) makes a change, that could easily break (either in spectacular fashion, or in a way that only surfaces when you deploy to a million users). Embracing well defined, thoughtful, and strict API guarantees is how we achieve modularity in the context of an ecosystem, not an "anything goes" mindset. Bevy (and ECS in general) already has the critique that it makes static guarantees harder, and there is the perception of hyper dynamic "wild west anything goes" design. We should not be leaning in to this when it comes to how we build our APIs. The Visibility CaseThe Someone might see the
This path is fraught. If we're lucky, this user's hacks (and the risks they pose) are scoped to just their project. But if this is done in the context of a plugin, and that plugin gets users, suddenly every consumer of the library is saddled with this risk (potentially with no knowledge that they are taking it on). This user would have been much better served by defining a new Or alternatively, if there is enough demand for a "contextless anything-goes Visibility component", that could absolutely be built. But it must done done intentionally by the upstream developers. Even if it feels simple, building a general purpose any-context ConclusionI understand the tinkerer's urge to be able to reach in and change anything in the engine. I don't even object to adding break-glass functionality to do this (especially if it is suitably "scary", ex: But when it is suggested that we embrace from a clear, user-facing perspective, that arbitrary permutations of well defined upstream context is allowed and encouraged, I can't help but be terrified. I've resisted this on basically all fronts in the engine, and I will continue to do so. This is an ecosystem health issue, a user experience issue, and a documentation issue. APIs are not "generic by default". They must be carefully made that way. And when we do make them that way that has big consequences for how they are used and consumed. Even if we could design our APIs in that way, I don't think we should (edit: in the majority of cases). Well-defined context and assumptions are what allow us to write code, especially when we are trying to build things together in a modular ecosystem. When asking "should I use |
@@ -1,4 +1,421 @@ | |||
<!-- Required Components --> | |||
<!-- https://github.com/bevyengine/bevy/pull/14791 --> | |||
First: buckle up because **Required Components** is one of the most profound improvements to the Bevy API surface since Bevy was first released. |
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.
Starting a thread to discuss my message above, in the interest of making this discussion manageable.
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 I'm sold: this is a compelling defense. I'll take a pass to see if we can help it come through clearly in the blog post itself, and make the "flexibility must be designed" point well.
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.
Thanks for the considered write-up @cart!
The schools of thought you have outlined seem a little unclear to me. For example, I would say that I consider components to be just data, but that behaviours are governed by the set of components on an entity. I would not say that I want to remove required components, rather that if we encourage library vendors to use required components for convenience, that they're going to seriously compromise the composibility inherent in Bevy's ECS.
I'm also not totally sure about the requirements you seem to be outlining.
You say that library/app authors shouldn't "mutate existing upstream concepts/assumptions" - but this is very vague. Obviously you shouldn't be modifying someone elses assumptions, but if it's something that library and app developers can modify then the mistake was upstream: they made an erroneous assumption.
A key example of this is the claim that if required components don't exist, libraries will be unable to maintain their invariants. Could you provide an example of an invariant that required components help to uphold?
As far as I can tell, components are always accessed optionally: you either directly call get
which returns an Option<>
, or you query for the component, which will only yield entities that match. The only edge-case I can think of where the invariants of a library could become invalidated is if they provide different queries to systems and expect them to act on the same set of entitites. For example, having a cleanup system with a more specific query, meaning it doesn't run for some entities and leaks. However, I would consider this a very clear cut library bug, which is very straight-forward to address.
Another thing you raise is "re-using components for different purposes". I would argue that in both cases, the specific semantic meaning of the component is important (and it's up to the consumer to not abuse that, or rather, if they try and use it for a different meaning, it should be clear that other systems might take it as the original meaning). However, if you do need to have entities with the same semantic aspect, but aren't planning to fulfil the contract of existing systems, I think it's desirable to be able to re-use the component (if people write their own render pipelines, say for point clouds, I would expect them to integrate with Visibility
in the same way that meshes & meshlets do).
I agree with your argument that building generic components is inherently more complicated, and I don't think that's something Bevy should be trying to do. However again, it feels like a non-sequitur.
It makes sense to me that you would require private components you need to maintain your invariants (which would be invisible to consumers of your library), but potentially include components which are part of your contract for better user experience (in lieu of an editor or authoring flow).
As we've discussed before, Transform
is an excellent example that I think should not be required but should be included. It seems intuitive to me that the rendering modules should only care about GlobalTransform
.
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.
they're going to seriously compromise the composibility inherent in Bevy's ECS.
The whole angle of my argument is that composability is not actually inherent in the ways that matter. Composability is something that is carefully created. Required Components don't get in the way of that. In fact, they help facilitate it by making the contract clearer.
Could you provide an example of an invariant that required components help to uphold?
My Visibility examples cover a variety of these. Things as simple as "All Players have Health" or "Health requires Character" also clearly fall into this category. Being able to rely on a conceptual "whole" composed by many parts is critical.
but aren't planning to fulfil the contract of existing systems,
This is part of the problem. The systems are just one part of the contract. How all the pieces relate to each other is the contract. And in most cases, the pieces have not been carefully designed to be consumed piecemeal at the arbitrary whims of downstream developers.
As we've discussed before, Transform is an excellent example that I think should not be required but should be included. It seems intuitive to me that the rendering modules should only care about GlobalTransform.
This does largely make sense to me. I think there is a reasonable case to be made for Transform to be "included" and not "required". But I think that is largely an exception to the rule, and GlobalTransform
is a clear case of being a well-defined largely "general purpose" concept. The "optional and included for convenience" category of component is something that will come up. I'm not fighting that. Only stating that (1) require
should be the default and the focus (2) the usage of include
requires careful thought.
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 whole angle of my argument is that composability is not actually inherent in the ways that matter.
In traditional ECSes, you do often get composability "for free" because systems only interact with components that are part of their contract (or their own private components). Often it's left as an exercise to the consumer to make sure that components required for follow-on behaviour are present (if they want that behaviour). Conversely, they are free to introduce their own behaviours that write to inputs or read from outputs of other systems.
My Visibility examples cover a variety of these.
You have provided examples of requirements, and if we assume that required components cannot be removed, you've provided invariants, but it's not clear to me that anything would depend on them. In those examples, what would be relying on the invariants?
How all the pieces relate to each other is the contract.
You need to be more specific here. Components can't relate to each other inherently as they're just data. Is your point here that the contract is that these components only appear together in archetypes?
Required components limit the archetypes that are allowed to exist. Queries already exist to limit the archetypes that systems act upon. It's already possible to ensure the behaviours you're referring to only operate on entities that do have all the components in question. So why is it important to disallow archetypes that only contain some of the components?
It seems to me that the same result could be achieved by using additional With<>
clauses in systems to depend on their downstream components, and including components by default rather than requiring them.
For the goal of improving new user experience, I think this has the same upsides. For the Bevy developers, I think it helps prevent accidentally creating behaviour people depend on. For developers, it seems less restrictive than required 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 replying up here but this is in relation to the thread of the topic about physics.)
@Jondolf there's an important nuance I'm worried you've missed here: I don't think that the dichotomy is between hard requirements and soft requirements.
From my point of view, both scenarios are offering hard requirements. An example of soft requirements would be automatically assuming defaults if the components were missing.
I think that include-by-default, with suitably strict queries provides both the hard requirements at execution, the removal of some foot-guns for new users, and allows flexibility without undermining invariants.
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.
In traditional ECSes, you do often get composability "for free" because systems only interact with components that are part of their contract
I fully acknowledge that traditional ECS helps facilitate composability in this way (we all know how ECS works here). My intent when I said "is not actually inherent in the ways that matter", was to express that this free-form mixing of contexts that ECS allows does not inherently solve the hard problems of building composable APIs (predictable behaviors, correctness, well defined scope, preventing people from stepping on each other's toes, etc).
but it's not clear to me that anything would depend on them. In those examples, what would be relying on the invariants?
Allowing someone to strip away the ViewVisibility / InheritedVisibility components is a signal to the user that stripping them out and using Visibility
standalone is supported, outside of a hierarchical or view context. That is not the case for the current implementation (for the reasons I stated). Those are the "invariants". I think it is important to call out that these "invariants" will show up by nature of building a specific thing in a specific context (ex: assuming that propagation happens only when all of the components are present in each member of the hierarchy). Many of them will be choices that were not consciously made.
How all the pieces relate to each other is the contract. Is your point here that the contract is that these components only appear together in archetypes?
Yes that is one of my points. It is a statement that "these components exist to collectively produce a set of behaviors, they are not designed to be used alone, and the implications of doing so have either not been explored or have been rejected". Restricting the archetype is a part of that (and a benefit of that), but it is just one expression of that set of values.
It seems to me that the same result could be achieved by using additional
With<>
clauses in systems to depend on their downstream components,
This is another one of my points. If Visibility
means "hierarchical / view visibility" only when the hierarchy and view components are also present, that means consumers needs to express that manually on every query (ex: via With
clauses) when they only want "hierarchical / view visibility". I'm arguing that it is a simpler / easier to use system if we just let Visibility
be the "hierarchical / view visibility" component. One piece in the system we've built, rather than a standalone piece of functionality.
"ECS queries using multiple components as filters to selectively enable logic" does not buy us "free composable behavior", as evidenced by Visibility
silently blocking hierarchy propagation, even in absence of the view / hierarchy components. There is an infinite number of ways that a system could "partially break" when attempting to use a component out of the context it was original built for.
Even if (by happenstance) stripping out components built to work together produces the desired behavior, and even if you manage to layer new behavior on top (ex: TransdimensionalVisibility
or something), you've still made the weirdness factor significantly higher, by nature of fundamentally changing what it means to interact with Visibility
in some cases. Then sometimes when you change Visibility
, it is doing "transdimensional visibility" and sometimes it is doing "hierarchical / view visibility".
There may be an intersection between "transdimensional visibility" and "hierarchical / view visibility". But the point is the author of Visibility
did not build it with that scenario in mind or define the set of rules it would follow in that context. Maybe we want a more general Visibility
component that can accommodate "transdimensional visibility" (and other such scenarios). To support that, we need to discuss what a component like that would look like, ensure all of the functionality we have built so far is compatible with the scenarios we want to cover, document how it is intended to be used, and then ensure the changes we make going forward don't break those scenarios.
For the goal of improving new user experience, I think this has the same upside
It does not solve the fundamental issue, which has been stated a number of times already. My goal for the system (and how we talk about it) is to strongly guide people toward building cohesive systems that are easy to use / understand / document. That means intentional API design. If someone isn't intentionally baking "generic-ness / optional-ness" into their design, we should not assume it is there.
Something that I'd like Bevy maintainers to keep in mind here is that Bevy doesn't currently have a physics engine, but one day it will (probably). And if that day comes, and if the physics engine looks anything like engines I've worked with in the past, you will absolutely be making heavy use of required components, not included components, for regular gameplay code. You want a projectile? You need some type of Rigidbody component, this cannot be optional. You'll also need some type of collider component, this cannot be optional either. You want two bodies to be constrained together by the physics engine, like a car and its door? You need a hinge or some other constraint component and it must have access to two entities that are guaranteed to have a RigidBody component and a collider component. So, while the difference may be immaterial to 99% of users today, I am of the opinion that it will not be the case forever and these examples are not in the territory of being esoteric or specialized tools. They're very common gameplay mechanics that many games have. And to Cart's point, in order to properly design a physics engine, you need well defined, thoughtful, and strict API guarantees like you get with hard requirements as I've outlined in my examples. |
@EmileGr, I think I understand your examples, but I'm not sure why you think required components would help. I would've expected most of the things you listed to be prefabs rather than being tied to a particular component. The hinge example particularly worries me, because you can't use requirements to make sure that the targets of a component fulfil requirements. (I'm presuming from past experience here, that a hinge is likely to be a component with two My expectation (and particularly how existing ECS physics implementations work) is that certain behaviours would only apply if you matched the criteria. If you want a projectile which uses forces to move around, then yes, you also need a rigid body - and this will be reflected in the queries of the systems involved. This gives you a strict API with strong guarantees. |
@ricky26 I don't want to derail this thread too much. This should really be a discussion about release notes. My apologies in advance to all of the maintainers! Alas, I'm not on the Discord, so I'll try to clarify my point here and we can take this up somewhere else, if you wish.
You are, of course, correct that something like a projectile or a door would be a prefab in practice. My projectile anecdote was to demonstrate that in order to make a such a prefab, one would need both a rigid body and a collider. A collider may exist on its own for static objects in your world. A rigid body , however, cannot exist without a collider. The rigid body tells the physics engine that the object may move according to forces and respond to collisions. Well alright, how can it respond to collisions if we don't know what collision shape it has? Thus a collider is a required component of a rigid body and thus we may safely make the assumption that an entity with a rigid body also has a collider and you may design your APIs or gameplay code or whatever else you're doing accordingly when you use required components.
This can be a matter of implementation and some physics engines differ in this regard, but often what you'll see is that Entity A (our door) has a rigid body, collider and the hinge component itself and Entity B (our car) only has a rigid body and a collider (which is then referenced by the hinge). Sometimes Entity B only has a collider in the event that it is a static wall or something like that. The point, albeit poorly made, now that I'm reading my original message again, is with this implementation, a rigid body will be a required component of a hinge, why? Because we know it has to move, respond to forces and collisions. Thus it must have a rigid body. A collider is a required component of a rigid body and thus it must have a collider. Again, you can now safely make the assumption that any entity with a hinge will also have a rigid body and a collider and you may design your APIs, gameplay code or whatever else you're doing accordingly when you use required components.
Correct again. You can, however, design APIs around the knowledge that they will all exist together once a "useful" hinge does exist in your world. Moreover, when Bevy gets an editor, wouldn't it be nice if you added a hinge and it added the rigid body and collider for you if they didn't exist? Then you can edit their reflectable properties until they do become useful.
And a collider. Now, could I query for the presence of both? Of course. Should I necessarily have to? No. Once I've queried for a rigid body, it would be very nice to know that the assumption of a collider being present also holds true. I'll close this off by saying that my point wasn't so much that you can't do any of these things without required components. You can, as you've correctly pointed out. And you do make some very good points there. Truly. Rather, my point is that hard requirements between components are a real thing in games, sometimes out of true necessity, other times in order to have less edge cases that need to be accounted for. And optional/"included" components wouldn't necessarily be the go to in a world where you're using a physics engine heavily for your gameplay. Really, not a very "deep" point. And I apologize if I made it sound like I was taking a more aggressive stance than that. Definitely not my intention. |
Since we're discussing physics, I feel obligated to respond to some of these things and give my take on it 😅 First, some corrections. I put these under a collapsible since they're not entirely relevant to the overall topic.
No, they totally can. Rigid bodies do not inherently require colliders, see for example Rapier, Avian, Box2D, PhysX, Bepu... And even if they were "required", required components don't make sense for this since there is no sane default for a collider shape. A rigid body is just a physics object that takes part in the simulation. Dynamic rigid bodies have velocity and mass, and can respond to forces. It's perfectly valid to have rigid bodies that don't have collision behavior, but respond to gravity, external forces, and joints.
A hinge is just a joint that constrains the relative degrees of freedom of two rigid bodies. The hinge itself does not have to move, respond to collisions, or anything like that. The hinge should typically be its own entity, and store the entity IDs of the rigid bodies in a component. Then the solver just iterates through hinges, and solves the constraint if the entity IDs match the query. If you force joints to be on the same entity as the rigid body, you can't nicely constrain one entity to multiple entities, since you can't have duplicate components. It can be unclear which entity should have the joint, and despawning the rigid body would also despawn the joint, which is not always desirable. (Note: bevy_rapier does this, but there's an open issue to change it) Sometimes you want joints to point to invalid/non-existent entities, for example if you detach and later reattach the joint, either to the same entity or to a different one.
No; what type of rigid body and collider would they be? Dynamic, static, cuboid, sphere? There are no sane defaults here. And again, joints don't need rigid bodies, and they especially don't need colliders. Perhaps this is more of a philosophical question, but if you think of a real-life hinge constraining a door to a door frame, does it stop being a hinge if you remove the door? Or is it simply a hinge that has been temporarily detached from the other object? Reeling the discussion back to required components: For physics, there's a lot of ways these things could be designed. You could just do things like typical physics engines, and have large structs for rigid body and collider definitions, in which case you don't really need required components. Or, you could use a more ECS-driven approach where state for physics objects is split into many components. bevy_rapier is the former, storing physics state outside the ECS, with components primarily as a thin API layer to ergonomically interface with Rapier. I'll focus on the ECS-driven approach like in Avian, since that's more relevant to this discussion.
Overwhelmingly, I think these kinds of "related components" that make an entity actually behave like a rigid body / collider / joint should be required for them, not included. Not requiring things like position, velocity or mass properties for dynamic rigid bodies would make no conceptual sense, and breaking these assumptions made by the engine would completely break things, often in unexpected ways. For example, the average user could reasonably assume that e.g. joints would work without the bodies having velocity, since conceptually they just constrain positional and rotational degrees of freedom. However, with an impulse-based solver (the vast majority of physics engines), this is not the case. Finally, there are of course many components that can be entirely optional. This includes restitution, linear and angular damping, external forces, collision margin, sweep-based CCD... But these are largely unrelated to required or included components. ConclusionPhysics entities need several components to function in any sensible way. There should be well defined, strict API guarantees for what a In most cases where I do want to require or include components, I specifically want them to be required, not just included. I largely agree with Cart's take on preferring hard requirements by default, not just in physics either, but in general. That being said, I personally think components for specific implementation details should often not be required in the type definition, but the requirement should rather be registered by a plugin. For example, a |
@Jondolf I've followed your progress on Avian with great interest. Thank you for your comment, but like I mentioned in my previous post, we're actively derailing and disrupting a conversation that really has nothing to do with the PR at hand at this point. Could we please move this discussion elsewhere? I will briefly say that I don't disagree with any of your points or corrections. I've been modifying the Jolt physics engine to utilize Bevy's ECS as a hobby project. So, I'm well aware that you don't need collision response if all you're doing is calculating direct forces for a rigid body. It was for the sake of simplicity that I outlined a theoretical physics engine that would work like that to demonstrate how you would practically use required components to achieve something akin to inheritance through composition. Indeed, it was wrong of me to make a unilateral claim that a rigid body would need a collider at the level of the physics engine. However, if you look at my example of a projectile entity, it becomes clear that you would most likely use required components to mandate the existence of both a rigid body and a collider on said entity (and on most entities that can move). As to your comment on "no sane default" for colliders. I respectfully disagree. I think a box collider matching the AABB of the renderer would make a sensible default. That of course assumes that I renderer can be found, I know, I know. The editor could also prompt you to pick one. Same case with a hinge. Very gross oversimplification to get the point across. But mandating that your door entity requires a hinge, rigid body and collider would also not be unusual. Again, my point was simply that you would likely make extensive use of required components, not included or optional components to make these relatively common objects found in many games. That was the context of my response to Alice's remark. I really would not like to design an entirely functional physics engine from scratch, off the top of my head, in a PR comment for what has become totally unrelated to the release notes haha. I'll continue this in a separate discussion if you'd like, but this will be my final comment here. This is not the time nor place for this discussion. |
My main point was also that I think physics would extensively use required components, not "included" components, and I wanted to explain why/how, which is somewhat relevant for this PR considering the main point of controversy seems to be whether require vs. include should be the default, and how composability works here (I would've responded in Cart's thread, but the physics stuff was being discussed here...) Apart from the collapsed part, I was more-so responding to the pushback (not from you) on "hard requirements by default" in the context of physics since that's what was being used as the example. A lot of the same points apply for Bevy in general, not just physics. But yes, agreed we probably shouldn't continue discussing this 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.
Quick copy editing pass
release-content/0.15/release-notes/14791_Required_Components.md
Outdated
Show resolved
Hide resolved
release-content/0.15/release-notes/14791_Required_Components.md
Outdated
Show resolved
Hide resolved
release-content/0.15/release-notes/14791_Required_Components.md
Outdated
Show resolved
Hide resolved
release-content/0.15/release-notes/14791_Required_Components.md
Outdated
Show resolved
Hide resolved
release-content/0.15/release-notes/14791_Required_Components.md
Outdated
Show resolved
Hide resolved
release-content/0.15/release-notes/14791_Required_Components.md
Outdated
Show resolved
Hide resolved
release-content/0.15/release-notes/14791_Required_Components.md
Outdated
Show resolved
Hide resolved
release-content/0.15/release-notes/14791_Required_Components.md
Outdated
Show resolved
Hide resolved
release-content/0.15/release-notes/14791_Required_Components.md
Outdated
Show resolved
Hide resolved
Co-authored-by: Joona Aalto <[email protected]>
Based on the discussion here and in bevyengine/bevy#16267 (comment), I'm moving forward on this in its current form for 0.15. We will discuss if / how the other component relationships are introduced during the 0.16 cycle, rather than trying to rush that conversation (and implementation) right before release. We can iterate on this content in followup prs if we need to. |
Fixes #1725