|
| 1 | +# Feature Name: `derive_component` |
| 2 | + |
| 3 | +## Summary |
| 4 | + |
| 5 | +Remove blanket impl from `Component` trait, and require it to be manually implemented or derived using `#[derive(Component)]`. |
| 6 | +Extend the `Component` trait to declare more information statically, instead of requiring runtime registration. |
| 7 | + |
| 8 | +## Motivation |
| 9 | + |
| 10 | +The `Component` trait is being used inside Bundles and Queries. It's usage in Queries is enforced by the type system. |
| 11 | +Unfortunately, because it's automatically derived, almost every type can be treated as a component, including Bundles. |
| 12 | +That means `Component` APIs don't really have any way to prevent misuse, like `commands.insert(bundle)`. Right now |
| 13 | +it's very easy to end up with that code because of a typo, as the `Bundle` api methods are very similarly named. |
| 14 | + |
| 15 | +There are also other pitfalls, like primitive types being automatically `Component`s, but without clear meaning what they represent. |
| 16 | +This is an especially severe issue for function pointer types. It's easy to use an enum variant or newtype constructor as |
| 17 | +a component, like `.insert(MyEnum::Variant)`, without realising it. This leads to very hard to spot bugs without any hint |
| 18 | +by the compiler or at runtime that something went wrong. This is easily preventable by not implementing the `Component` trait for those types. |
| 19 | + |
| 20 | +We also already have `#[derive(Bundle)]` and others. Adding that to `Component` keeps the syntax similar across the board. |
| 21 | + |
| 22 | +Apart from requiring explicit opt-in, `derive` gives us a natural place to define more metadata about specific component type. |
| 23 | +One way to use that is to declare component's Storage type statically, instead of requiring manual registration of that metadata |
| 24 | +into the world. |
| 25 | + |
| 26 | +```rust |
| 27 | +#[derive(Component)] |
| 28 | +#[component(storage = "SparseSet")] |
| 29 | +struct MyComponent(..); |
| 30 | +``` |
| 31 | + |
| 32 | +This enables the compiler to perform much more optimizations in the query iteration code, as a lot of the code paths can be statically eliminated. |
| 33 | + |
| 34 | +## User-facing explanation |
| 35 | + |
| 36 | +In order to define your own component type, you have to implement the `Component` trait. The easiest way to do that is using a `derive` macro. |
| 37 | + |
| 38 | +```rust |
| 39 | +#[derive(Component)] |
| 40 | +struct HitPoints(u32); |
| 41 | +``` |
| 42 | + |
| 43 | +If you forget to properly annotate your component type, the compiler will remind you |
| 44 | +of that as soon as you try to insert that component into the world or query for it. Apart from type safety, this also provides a visual indication that the given type |
| 45 | +is intended to be directly associated with entities in the world. |
| 46 | + |
| 47 | +By default, your component will be stored using Dense storage strategy. |
| 48 | +In order to declare your component's storage strategy manually, you have to provide an additional attribute |
| 49 | +to specify the component settings. |
| 50 | + |
| 51 | +```rust |
| 52 | +#[derive(Component)] |
| 53 | +#[component(storage = "SparseSet")] |
| 54 | +struct Selected; |
| 55 | +``` |
| 56 | + |
| 57 | +### Migration strategy |
| 58 | + |
| 59 | +All your component types must be annotated with a derive. |
| 60 | + |
| 61 | +```diff |
| 62 | ++ #[derive(Component)] |
| 63 | + struct MyComponent { .. } |
| 64 | +``` |
| 65 | + |
| 66 | +You can no longer use primitive types (e.g. `u32`, `bool`) as components. |
| 67 | +Wrap them in a custom struct, and mark that as a component. |
| 68 | + |
| 69 | +```diff |
| 70 | ++ #[derive(Component)] |
| 71 | ++ struct HitPoints(u32); |
| 72 | + |
| 73 | +- commands.entity(id).insert(100); |
| 74 | ++ commands.entity(id).insert(HitPoints(100)); |
| 75 | +``` |
| 76 | + |
| 77 | +Remove all registration of components. Make sure to correctly annotate components that were previously registered with `SparseSet` storage type. |
| 78 | + |
| 79 | +```diff |
| 80 | +- world.register_component( |
| 81 | +- ComponentDescriptor::new::<MyComponent>(StorageType::SparseSet) |
| 82 | +- ).unwrap(); |
| 83 | + |
| 84 | ++ #[derive(Component)] |
| 85 | ++ #[component(storage = "SparseSet")] |
| 86 | + struct MyComponent { .. } |
| 87 | +``` |
| 88 | + |
| 89 | +## Implementation strategy |
| 90 | + |
| 91 | +- remove the existing blanket impl |
| 92 | +- implement basic derive macro that just implements a marker trait |
| 93 | +- add associated `Storage` type to the `Component` trait |
| 94 | +- provide a way to specify the `Storage` type using an macro attribute. |
| 95 | +- use the associated `Storage` type inside query iterators to statically determine |
| 96 | + if the query is sparse or dense. |
| 97 | + |
| 98 | +## Drawbacks |
| 99 | + |
| 100 | +Requiring extra annotation on every component type adds some boilerplate. |
| 101 | + |
| 102 | +## Rationale and alternatives |
| 103 | + |
| 104 | +- instead of `derive`, use an attribute macro. |
| 105 | +- Do nothing and keep registering metadata at runtime. Live with the branching inside query iterators. |
| 106 | +- Require manual implementation of `Component` trait without providing derive macro. |
| 107 | +### Why not use a manual implementation of `Component` to set the storage type? |
| 108 | + |
| 109 | +As shown in [this playground link](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=ca85c5d3c561ef9d1feec143bbcd9611), we could force users to write out a manual implementation of the `Component` trait when they wanted to change the storage type. |
| 110 | + |
| 111 | +This has a couple advantages: |
| 112 | + |
| 113 | +1. More direct and less magical, at least for advanced Rust users. |
| 114 | +2. No configuration DSL to maintain. |
| 115 | +3. Better type checking. |
| 116 | + |
| 117 | +However, it has several more serious disadvantages, which outweigh those: |
| 118 | + |
| 119 | +1. Manual implementations of `Component` will break (often in dozens of places scattered around the code base) whenever we make breaking changes to the `Component` trait. Derive + attribute macros will continue working flawlessly, except where they directly touch the breaking changes. |
| 120 | +2. Longer boilerplate, especially as the `Component` trait grows. This is particularly frustrating as you need to add / remove this repeatedly when benchmarking perf as the actual net impact is very fact-specific. |
| 121 | +3. Exposes internal-ish implementation details to users who really don't care. |
| 122 | + |
| 123 | +### Comparison of methods of defining storage type in `Component` trait |
| 124 | + |
| 125 | +There are several possible ways to define the storage type, as shown in [this playground link](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4a2d47173f68137a34c31987d9d46bfa). |
| 126 | + |
| 127 | +While any approach would work, an associated type is safer and offers better discoverability, due to the use of the type checker. |
| 128 | +In addition, you can't assert specific associated const value in the trait bounds, which may be limiting in the future. |
| 129 | +The slight increase in verbosity is worth it for these benefits. |
| 130 | +## Unresolved questions |
| 131 | + |
| 132 | +- How to support dynamic components for future scripting layer? |
| 133 | + |
| 134 | +## Future possibilities |
| 135 | + |
| 136 | +The Component derive could later lead to automatic derives for reflection, or in general serve |
| 137 | +as a way to reduce boilerplate from new features of the engine added in the future. |
0 commit comments