Skip to content

Commit a1e8a68

Browse files
FriziMinerSebasalice-i-cecile
authored
Require #[derive(Component)] for component types (#27)
* add derive component RFC * remove leftover part of the template * Apply suggestions from code review Co-authored-by: MinerSebas <[email protected]> * add justification for derive macro DSL Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: MinerSebas <[email protected]> Co-authored-by: Alice Cecile <[email protected]>
1 parent 4860ce9 commit a1e8a68

File tree

1 file changed

+137
-0
lines changed

1 file changed

+137
-0
lines changed

rfcs/27-derive-component.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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

Comments
 (0)