Description
With the introduction of an opt-in Component
trait in #2254, external types can no longer be directly included as components.
In the standard scenario, we have 4 parties:
- Bevy
- the external crate (e.g. Rapier)
- the interface crate (e.g. bevy_rapier)
- the end user who just wants to make a game
Due to Rust's orphan rules, only Bevy or the external crate can implement Component
for arbitrary external crate structs directly. The interface crate or end user can only bypass this using a wrapper type, and using a technique known as extension traits.
Users need to be able to use external types as components, in one form or another.
There are several options for this:
- Bevy implements
Component
for external types. This is impossible, as this set is unknowable, and using overly broad blanket impls causes all of the issues that [Merged by Bors] - Implement and require#[derive(Component)]
on all component structs #2254 solved. - The external crate implements
Component
for the relevant types, possibly hidden behind a feature flag. This is the solution used forserde
and other ecosystem crates. This is ideal, but not always going to happen, as it is outside of the control of anyone who cares about Bevy. - The interface crate or end user writes a manual wrapper type around the external component. This prevents accidental conflicts between different uses of the same component, but is heavy on boilerplate and conceptual indirection.
- The interface crate or end user writes an
ExternalComponent<T, StorageType>
struct and implements component on it. This avoids accidental conflicts and reduces boilerplate, but reduces the type safety of the API in some of the ways that [Merged by Bors] - Implement and require#[derive(Component)]
on all component structs #2254 solved. - Bevy writes an
ExternalComponent<T, StorageType>
struct and implements component on it. This avoids ecosystem fragmentation, but has most of the same problems as 4. Extending this becomes increasingly onerous as we add more configuration to component types.
Given that 1 is impossible, and 2 is not enforceable, we are left with a choice between 3-5.
The tentative consensus from the active ECS contributors is that 3 is the least bad option, with the encouragement to use Deref
and DerefMut
on the wrapper types until delegation is one day (🤞🏽) added to Rust.
4 is a particularly bad solution due to the impact on type safety and difficulty extending it. 5 is less bad than 4 in some ways, as getting 4 correct is going to be frustrating for new users and its easy to screw up and make a completely unconfigurable wrapper type. However, by including an ExternalComponent
type in Bevy itself, we are effectively endorsing that solution.
Thus we should:
- clearly document how to include external component types in the documentation for both end users and plugin authors
- investigate disabling the naive use of 4 by forbidding unconstrained generic derives of the Component trait (credit to @DJMcNab)
- investigate publishing a tool / crate to more quickly allow users to wrap types in Bevy components (and resources, as the same issues exists for
Reflect
even without aResource
trait), credit to @BoxyUwU