Skip to content

Make Component opt-in #1843

Closed
Closed
@concave-sphere

Description

@concave-sphere

What problem does this solve or what need does it fill?

Currently, Component is auto implemented for any type that is Send + Sync + 'static. This has some benefits:

  1. Convenience
  2. Allows direct use of pre-existing types as components

But there are also some downsides:

  1. There's no way to prevent Bundles from being used with Component APIs, such as in commands.insert(bundle). This results in confusing and hard to track down bugs for newbies, and the number of "danger spots" you have to learn to step around as part of the Bevy learning curve.
  2. It's easy to create an unclear architecture where it's not obvious what's supposed to be a component.
  3. Using foreign types as components is likely a land mine / attractive nuisance, because another package could come along with the same idea and conflict with your use. It would IMO be better to discourage this pattern so users don't shoot themselves in the foot.

Number one is hopefully obvious. I haven't written a large Bevy program yet, but based on my experience in other domains, numbers 2 and 3 are likely to become a nuisance. Allowing arbitrary types to implicitly get used as a channel for passing data sideways works great for small programs, and rapidly becomes unmanageable as the system size expands. At my workplace we can mitigate the danger with strict code policies, but I don't think that strategy will work with crates.io.

What solution would you like?

Don't auto implement Component. Instead, provide a derive macro:

#[derive(Component)]
struct MyCounter(i32);

The trait impl it expands to is trivial:

impl Component for MyCounter {}

This is almost as convenient as the existing code, while ensuring that types can only be used as Components if they were intended for that purpose. By default, it prevents using a Component as a Bundle (since you'd have to opt in to both types).

There is a question of what to do with generic types. I would suggest this:

#[derive(Component)]
struct MyGenericComponent<T>(T);

// Expands to:
impl<T> Component for MyGenericComponent<T>
    where T: Send+Sync+'static {
}

I didn't require <T> to be a component in this case: by wrapping T in MyGenericComponent, we've declared our intent to make it a Component. The orphan rule ensures that only the owner of MyGenericComponent<T> can implement Component on it (either as a blanket impl or as an impl for one instance). It would be possible to be stricter, but I think this is a good compromise between convenience and safety.

What alternative(s) have you considered?

I don't see any way to fix the problems listed above as long as Component is auto implemented, but there may be some other trickery that my Rust type fu is too weak to see.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ECSEntities, components, systems, and eventsA-ReflectionRuntime information about typesC-UsabilityA targeted quality-of-life change that makes Bevy easier to useS-Needs-Design-DocThis issue or PR is particularly complex, and needs an approved design doc before it can be merged

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions