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

0.15 Release Notes: Required Components #1779

Merged
merged 4 commits into from
Nov 11, 2024

Conversation

cart
Copy link
Member

@cart cart commented Nov 6, 2024

Fixes #1725

@alice-i-cecile
Copy link
Member

We're going to need to overhaul these if / when we merge bevyengine/bevy#16267 but we shouldn't let that block this PR.

@cart
Copy link
Member Author

cart commented Nov 6, 2024

We're going to need to overhaul these

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.

@omaskery
Copy link

omaskery commented Nov 6, 2024

We're going to need to overhaul these

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 required the front-and-center concept, and only express the others as a footnote, that it will lend itself to people reaching primarily for required in all cases. If I've understood your opinions correctly, we want to express something like:

  • In a library crate:
    • Use suggested whenever you think it will help with discoverability/documentation, there's no downside to using this. Has no impact on Bevy's behaviour at runtime, but is available for reflection in tooling.
    • Use included whenever you think a type is almost always used with another type, but power users can opt out of these as needed, so you can safely "over use" this as it will never stop somebody doing what they want.
    • Use required only on "driver" components that are designed to invoke specific functionality in your crate and fundamentally won't work without the required components. These cannot be overridden, so think carefully before marking a component as required. Whilst not always sensible/possible, it is ideal if power users can choose to bypass the driver component and directly use the underlying components to achieve some or all of the same functionality.
  • In a binary crate:
    • Do whatever you want as it's only you that has to deal with the consequences and you can freely change the strength of the relationship whenever you want :P

@cart
Copy link
Member Author

cart commented Nov 6, 2024

that it will lend itself to people reaching primarily for required in all cases

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)

@alice-i-cecile
Copy link
Member

alice-i-cecile commented Nov 7, 2024

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 Interaction component for Button without forking Bevy helps us avoid surprising bug reports or really in line with our goals for modularity and flexibility of Bevy.

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 Visibility and ComputedVisibility, they're aware that Bevy's built-in rendering is going to break, and that behavior is not surprising to them.

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.

@cart
Copy link
Member Author

cart commented Nov 7, 2024

The Schools of Thought

I 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:

  • School 1: Components are "just data". People should be able to use them in whatever context they want, regardless of the initial authorial intent, the components they were built to work with, or the default behaviors. For example, remove the InheritedVisibilty required component from Visibility and use it standalone in a new context (perhaps related, perhaps not).
  • School 2: Components are "data plus behaviors". Adding a component is an action taken by a user to opt in to a specific predictable set of behaviors.

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).
b. How do I add new Component/Behavior context (new components with behaviors driven by new systems).
c. How do I configure existing Component/Behavior context in ways pre-defined and explicitly supported by their upstream author (ex: disabling optional functionality, either components or 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:

  • If a library author mutates existing upstream concepts/assumptions, that could very easily make their library incompatible with the rest of the ecosystem, or break it subtly.
  • If a Bevy app developer mutates existing upstream concepts/assumptions, that could very easily (and very subtly) break arbitrary upstream behaviors.

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 Case

The Visibility case you bring up is a great example of this. We have defined Visibility to be the "hierarchical view visibility configuration component". Lets run through a scenario:

Someone might see the Visibility component and think "I want to use Visibility for custom visibility logic in my App / Plugin. I don't want the InheritedVisibility or ViewVisibility components, or the hierarchical or view-computed behaviors they bring, so I'll remove them from the requirements".

  1. This is already weird, as Visibility::Inherited is one of the enum variants, which makes no sense outside of hierarchical visibility. What if we add more fields later that also rely on that context (like Visibility::InheritRoot)?
  2. Any present systems that assume Visibility and InheritedVisibility exist together either stop running (ex: due to queries no longer matching) or crash due to assumed existence. Even if there isn't an immediate break due to, say a get::<InheritedVisibility>().unwrap(), the fact that arbitrary logic is no longer running could be catastrophic. One real world, subtle, behavior this introduces immediately is that any entity spawned this way silently breaks the propagation of visibility when inserted in a "normal" context. Plenty of other new open questions like "how will removing ViewVisibilty affect extraction / render logic". I certainly don't know off the top of my head.
  3. Any new systems added in the future by 3rd party plugins or upstream bevy developers that rely on any of the assumptions of the Visibility / InheritedVisibiltiy / ViewVisibility pairing as it exists today could break down. Will the user's custom paired down and extended Visibility scenario break how visibility is presented or behaves in the upcoming editor? I have no clue!

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 IsVisible(bool) component, free from the upstream context (and massive amounts of existing upstream Bevy and ecosystem code that depends on that context).

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. Visibility in its current form does a poor job of performing that function (by nature of encoding the "inheritance" context in the enum variant), and none of the systems that exist today were built under the assumption that Visibility can exist outside of a hierarchical or view context (and again, if you want to strip out that context, why not just define your own component so consumers of Visibility can continue to make the assumptions they make today?). When building a "context free" component (or more likely, just a component with a significantly wider "context") there is a cost paid on both sides. The creator of the component needs to ensure their code can safely accommodate the entire scope of the "wider context", they need to clearly communicate the bounds of that "wider context" via API boundaries and documentation, and they need to ensure every consumer of the component also supports the full range of those contexts.

Even if it feels simple, building a general purpose any-context BackgroundColor component is fundamentally a harder problem than restricting it to "BackgroundColor is how you set the background color of a Node". The any-context component is harder to communicate and operate around the bounds of the API (ex: querying for BackgroundColor now returns non-Node entities too unless you add a marker component to filter down), harder to change (a breaking BackgroundColor API change now breaks everyone using it instead of just Nodes), and easier to break contracts (we might accidentally or intentionally change BackgroundColor to add more UI-context assumptions, compromising non-UI scenarios, or we might not improve it for UI contexts, in the interest of not breaking non-UI contexts).

Conclusion

I 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: #[remove_require_risky(InheritedVisibility)]).

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 include or require for component X", you need to ask yourself "am I building a general purpose context free component with careful consideration of API boundaries and all potential future downstream use cases, where all present and future upstream and downstream systems built around this component relationship will continue to function if that relationship is broken or changed in any way" or "am I building the feature that I'm trying to build now in the context I'm working in now". Pretty much everyone can and should be thinking in terms of the latter and not the former. Not only because it is easier to accomplish from a developer perspective, but also because it is easier for consumers of our APIs to successfully, reliably, and safely use and extend them.

@@ -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.
Copy link
Member Author

@cart cart Nov 7, 2024

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.

Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

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 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.

Copy link
Member Author

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.

@EmileGr
Copy link

EmileGr commented Nov 7, 2024

Hmm. I think that "included components" should be the go-to tool by default

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.

@ricky26
Copy link
Contributor

ricky26 commented Nov 7, 2024

@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 Entity references and other parameters.) Even if you could, it's not like you could generate two useful rigid bodies with colliders with no input.

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.

@EmileGr
Copy link

EmileGr commented Nov 8, 2024

@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.

I would've expected most of the things you listed to be prefabs

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.

The hinge example particularly worries me, because you can't use requirements to make sure that the targets of a component fulfil requirements.

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.

it's not like you could generate two useful rigid bodies with colliders with no input.

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.

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 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.

@Jondolf
Copy link
Contributor

Jondolf commented Nov 8, 2024

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.

@EmileGr

A rigid body, however, cannot exist without a collider.

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 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

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.

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?

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: RigidBody, Collider, and HingeJoint are "driver components" or "defining components". What's more relevant here is what components they require to function like rigid bodies, colliders, and joints, and whether those components should be "required" or "included".

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.

  • In Avian, all rigid bodies and colliders must have the Position and Rotation components, representing the global physics position. Without these, the engine is basically useless, since the internals rely on them so heavily. These should be required.
  • Static rigid bodies should not require velocity or mass since they'd do nothing on them, but dynamic bodies absolutely must have them. Otherwise, they just aren't dynamic bodies at all, and wouldn't behave like expected. Gravity does nothing, forces don't work, collisions don't work, joints don't work, and so on. Velocity and mass should be required for dynamic rigid bodies.
  • Colliders require positions, AABBs, and so on to function like actual colliders. Otherwise, they do nothing.
  • Then there are components required by internals, like "position delta accumulated during the timestep", "velocity from before the solver ran", and so on. Since these are implementation details, these requirements should probably not be registered by the components themselves, but rather by the relevant solver plugins using runtime required components; this way, the engine could theoretically be solver-agnostic, and you could swap it freely. But I prefer these being strict rather than loose requirements when the relevant plugins are enabled.

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.

Conclusion

Physics entities need several components to function in any sensible way. There should be well defined, strict API guarantees for what a RigidBody or Collider is and what components it needs to function like expected. This is not entirely exclusive with composing some functionality manually with individual components either, and you could still support e.g. Velocity as a standalone component that works even on entities that aren't rigid bodies. What's relevant is what an actual rigid body is and what it requires to function like one.

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 Hinge might only require HingeSolverData if an ImpulseSolverPlugin is enabled. This makes things implementation agnostic, and means you can explicitly opt out of the requirement by disabling the plugin / replacing it with an alternative, without breaking upstream assumptions, as long as plugin boundaries are well defined.

@EmileGr
Copy link

EmileGr commented Nov 8, 2024

@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.

@Jondolf
Copy link
Contributor

Jondolf commented Nov 8, 2024

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 :)

Copy link
Contributor

@Jondolf Jondolf left a 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

@cart
Copy link
Member Author

cart commented Nov 11, 2024

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.

@cart cart added this pull request to the merge queue Nov 11, 2024
Merged via the queue into bevyengine:main with commit f8c775e Nov 11, 2024
10 checks passed
@cart cart deleted the 0.15-required-components branch November 11, 2024 22:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Write release notes for PR #14791: Required Components
8 participants