Skip to content

External components with #[derive(Component)] #2966

Closed
@alice-i-cecile

Description

@alice-i-cecile

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:

  1. 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.
  2. The external crate implements Component for the relevant types, possibly hidden behind a feature flag. This is the solution used for serde 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.
  3. 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.
  4. 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.
  5. 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 a Resource trait), credit to @BoxyUwU

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ECSEntities, components, systems, and eventsC-DocsAn addition or correction to our documentationC-UsabilityA targeted quality-of-life change that makes Bevy easier to use

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions