Description
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:
- Convenience
- Allows direct use of pre-existing types as components
But there are also some downsides:
- There's no way to prevent
Bundles
from being used withComponent
APIs, such as incommands.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. - It's easy to create an unclear architecture where it's not obvious what's supposed to be a component.
- 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.