Skip to content

More forms of related components: suggested and included components #16267

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

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

alice-i-cecile
Copy link
Member

@alice-i-cecile alice-i-cecile commented Nov 6, 2024

Objective

As discussed in #16246, required components are a great first step, but it makes sense to use this machinery for less stringent relatonships.

Similarly, #15580 complains that required components are too restrictive for power users with unusual needs, and should be able to be disabled at runtime.

Solution

As discussed on Discord, I've added two new variants of required components (collectively, related components):

  • Suggested Components: just docs!
  • Included Components: like Required Components, but less strict.

See the Relatedness docs included with this PR for the exact behavior.

Fixes #16246; fixes #15580.

Testing

So far, it compiles...

TODO

  • write tests
  • double check main docs
  • add removal machinery
  • try to make the error handling nicer
  • rename related components to something that can't be confused with relations (probably linked or affiliated components)
  • split suggested components out from this PR

@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Nov 6, 2024
@alice-i-cecile alice-i-cecile added this to the 0.15 milestone Nov 6, 2024
@alice-i-cecile alice-i-cecile requested a review from cart November 6, 2024 19:53
@alice-i-cecile alice-i-cecile changed the title More forms of related components: suggested and included More forms of related components: suggested and included components Nov 6, 2024
Copy link
Contributor

@omaskery omaskery left a comment

Choose a reason for hiding this comment

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

This is my first Bevy PR, so not approving, just leaving comments. I thought I'd try and help out with this one since I've been so chatty on this subject on Discord!

However, I'm not familiar enough with Bevy internals to be too confident.

I've mostly identified things that escaped the find & replace (or got accidentally caught in the find & replace blast radius!) in comments. I might have commented on too many places (it's hard to tell!) and I've almost certainly missed some, too! Your mind starts to swim looking for all the "required", "related", "relations", "relatedness", etc! 😁

There are only a couple of places I've identified actual variables or code changes that might want to be made.

As I've mentioned in some of the comments, it's hard to discuss this refactor without wanting to use the word "relation" and "relationship", which is a bit of an unfortunate terminology overlap with the ongoing work on Bevy relationships. Not really sure what the winning move is here.

We could also do with figuring out the terms that more generally replace "requiree" and "required". Tricky!

Also: I am assuming that this PR is "the bare minimum to introduce the user facing concepts and make it vaguely work", because currently there are some things that are either missing or I'm too unfamiliar with Bevy's internals to understand:

  • No tests for the new functionality, just porting the existing tests to the new API.
  • The included and suggested components might not be exposed publicly, or at least not to the same degree that there are getters for required components.
  • There doesn't seem to actually be any API yet for opting out of (or opting in to) included components.

I'm not familiar enough with Bevy's internals to understand exactly how this change ensures that required & included are added, but suggested aren't, and (as I say above) how included can be toggled. So someone else will have to verify that aspect of the PR!

Hope I've helped and not just added noise :)

@@ -913,8 +923,8 @@ impl Components {
})
};
if registered {
let mut required_components = RequiredComponents::default();
T::register_required_components(id, self, storages, &mut required_components, 0);
let mut required_components = RelatedComponents::default();
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this variable be related_components?

&mut self,
id: ComponentId,
) -> Option<&mut RequiredComponents> {
) -> Option<&mut RelatedComponents> {
self.components
.get_mut(id.0)
.map(|info| &mut info.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.

Should this info.required_components be referring to related components?

///
/// In most cases, required components should be registered using the `require` attribute as shown above.
/// In most cases, required components should be registered using attributes like `require` as shown above.
/// However, in some cases, it may be useful to register required components at runtime.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// However, in some cases, it may be useful to register required components at runtime.
/// However, in some cases, it may be useful to register related components at runtime.

Maybe?

@@ -792,8 +792,8 @@ impl App {
/// # let mut app = App::new();
/// # app.add_plugins(MinimalPlugins).add_systems(Startup, setup);
/// // Register B as required by A and C as required by B.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// // Register B as required by A and C as required by B.
/// // Register B as related to A and C as related to B.

?

///
/// For the non-panicking version, see [`App::try_register_required_components`].
/// For the non-panicking version, see [`App::try_register_related_components`].
///
/// Note that requirements must currently be registered before `T` is inserted into the world
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Note that requirements must currently be registered before `T` is inserted into the world
/// Note that related components must currently be registered before `T` is inserted into the world

?

Suggested change
/// Note that requirements must currently be registered before `T` is inserted into the world
/// Note that relations must currently be registered before `T` is inserted into the world

?

Comment on lines 466 to 467
/// Note that requirements must currently be registered before `T` is inserted into the world
/// for the first time. This limitation may be fixed in the future.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Note that requirements must currently be registered before `T` is inserted into the world
/// for the first time. This limitation may be fixed in the future.
/// Note that relations must currently be registered before `T` is inserted into the world
/// for the first time. This limitation may be fixed in the future.

///
/// # Errors
///
/// Returns a [`RequiredComponentsError`] if `R` is already a directly required component for `T`, or if `T` has ever been added
/// Returns a [`RelatedComponentsError`] if `R` is already a directly required component for `T`, or if `T` has ever been added
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Returns a [`RelatedComponentsError`] if `R` is already a directly required component for `T`, or if `T` has ever been added
/// Returns a [`RelatedComponentsError`] if `R` is already a directly related component for `T`, or if `T` has ever been added

///
/// # Errors
///
/// Returns a [`RequiredComponentsError`] if `R` is already a directly required component for `T`, or if `T` has ever been added
/// Returns a [`RelatedComponentsError`] if `R` is already a directly required component for `T`, or if `T` has ever been added
/// on an entity before the registration.
///
/// Indirect requirements through other components are allowed. In those cases, any existing requirements
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Indirect requirements through other components are allowed. In those cases, any existing requirements
/// Indirect relations through other components are allowed. In those cases, any existing relations

let requiree = self.register_component::<T>();

// TODO: Remove this panic and update archetype edges accordingly when required components are added
if self.archetypes().component_index().contains_key(&requiree) {
return Err(RequiredComponentsError::ArchetypeExists(requiree));
return Err(RelatedComponentsError::ArchetypeExists(requiree));
}

let required = self.register_component::<R>();

// SAFETY: We just created the `required` and `requiree` components.
Copy link
Contributor

Choose a reason for hiding this comment

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

As previous: would be nice to come up with nouns for each end of the relatedness relationship.

/// Retrieves the [required components](RequiredComponents) for the given component type, if it exists.
pub fn get_required_components<C: Component>(&self) -> Option<&RequiredComponents> {
/// Retrieves the [related components](RelatedComponents) for the given component type, if it exists.
pub fn get_required_components<C: Component>(&self) -> Option<&RelatedComponents> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do there need to be new getters here to expose included and suggested components?

@alice-i-cecile alice-i-cecile added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Nov 6, 2024
@BD103
Copy link
Member

BD103 commented Nov 8, 2024

Non-blocking, but it may be nice to add / update examples on how to do this. I'm curious how to use this myself, but don't know enough about ECS internals to read the actual source!

&mut self,
constructor: fn() -> R,
) -> Result<(), RequiredComponentsError> {
relatedness: Relatedness,
Copy link
Member

Choose a reason for hiding this comment

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

Working very hard not to say relationship I see :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah :p "Related" needs a rename, as I brought up on Discord.

}
}

/// Retrieves the [required components](RequiredComponents) for the given component type, if it exists.
pub fn get_required_components<C: Component>(&self) -> Option<&RequiredComponents> {
/// Retrieves the [related components](RelatedComponents) for the given component type, if it exists.
Copy link
Member

Choose a reason for hiding this comment

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

This function should probably be called get_related_components now

/// Retrieves the [required components](RequiredComponents) for the component of the given [`ComponentId`], if it exists.
pub fn get_required_components_by_id(&self, id: ComponentId) -> Option<&RequiredComponents> {
/// Retrieves the [related components](RelatedComponents) for the component of the given [`ComponentId`], if it exists.
pub fn get_required_components_by_id(&self, id: ComponentId) -> Option<&RelatedComponents> {
Copy link
Member

@cart cart Nov 8, 2024

Choose a reason for hiding this comment

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

This function should probably be called get_related_components_by_id now

for component_id in component_ids.iter().copied() {
// SAFETY: caller has verified that all ids are valid
let info = unsafe { components.get_info_unchecked(component_id) };
required_components.merge(info.required_components());
related_components.merge(info.related_components());
Copy link
Member

@cart cart Nov 8, 2024

Choose a reason for hiding this comment

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

This merge is happening per component per bundle type. Including suggested components in this computation when they are essentially "debug info" is prohibitively expensive.

This is a symptom of a larger issue. Suggested components are fundamentally different in terms of purpose when compared to required and included components. Treating them as the "the same" here has produced multiple "bad" outcomes already:

  1. We're (accidentally) doing a bunch of unnecessary runtime work because we decided to treat them as if they are "the same".
  2. Suggested components shouldn't have constructors (because they aren't used at runtime), but we parse and store them as if they do.

I think they should probably have a separate code path to prevent these sorts of things.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, @Jondolf is of the same opinion WRT splitting out suggested components, and I think I'm largely convinced at this point.

let required_components = required_components
.0
related_components.remove_explicit_components(&component_ids);
let required_components = related_components
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't actually wire up included components. Given that this is computing final "runtime" results, I believe the "merge" that happens in this function should produce a single unified hashmap across both required and included components. Using the RequiredComponents (now RelatedComponents) collection is no longer correct in this new context.

.0
let inherited_requirements: Vec<(ComponentId, RelatedComponent)> = required_component_info
.related_components()
.required_components
Copy link
Member

Choose a reason for hiding this comment

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

This should register all related components, not just requires.


// Because the macro crate cannot access the `bevy_ecs` crate, we need to create our own equivalent.
// This should be kept in sync with the actual `bevy_ecs` crate!
enum Relatedness {
Copy link
Member

@cart cart Nov 8, 2024

Choose a reason for hiding this comment

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

This PR needs enough work, and is "involved" enough that I'm worried about rushing it in this late in the game. This feels like the kind of thing that deserves time to be settled and tested. Additionally, this framing has been on our mind for 3 days. I think it might be the right path forward, but the whole thing is still settling in my head. It feels like we're rushing into this.

Given how the required components conversation is going (in the release post PR), I think we can swing require-only for 0.15, especially if we add the "break-glass "#[remove_require_risky(Transform)]". Given that this whole "other related components" effort is motivated by implementing the removal behavior for include to nail down the difference between the relationships, and given that this PR does not yet implement that behavior, I think our limited resources / time / risk tolerance should be spent on implementing the removal functionality (remove_require_risky). That will already be reasonably invasive.

That then gives us a whole cycle to decide what the global picture should look like, ensure the implementation is sound, and actually make use of new tools like include ( I don't think we have the time in 0.15 to go through the engine and decide what deserves require vs include).

In the 0.15 release post, we could communicate that we plan on adding additional relationship tools for more flexibility.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, I'm on board with that decision. This is involved enough and I have enough to do that I'm not keen on pushing it in last minute.

@alice-i-cecile alice-i-cecile modified the milestones: 0.15, 0.16 Nov 8, 2024
@alice-i-cecile
Copy link
Member Author

This is more involved than I anticipated; bumping to 0.16 to avoid shipping bugs (and also to prioritize migration guide + release notes).

@BenjaminBrienen BenjaminBrienen added the D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes label Jan 7, 2025
@alice-i-cecile alice-i-cecile removed this from the 0.16 milestone Feb 26, 2025
@alice-i-cecile alice-i-cecile added S-Adopt-Me The original PR author has no intent to complete this work. Pick me up! and removed X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Feb 26, 2025
@hukasu
Copy link
Contributor

hukasu commented Mar 9, 2025

did not read all of it, but does it include an idea of these components are incompatible, and one of them is not going to work while the other is present? (refer to #17402)

@alice-i-cecile
Copy link
Member Author

It does not. That's a form of #1481, which this PR covers some of, but not that element. I agree that that would be nice though!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Adopt-Me The original PR author has no intent to complete this work. Pick me up!
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Suggested / related components Support removing a removed Component's required components
7 participants