-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make Component opt-in #1843
Comments
This has been brought up in other contexts before, and your arguments are valid. Avoiding the bundle-component confusion would be particularly nice, and negative trait bounds will be an eternity to hit stable. The main concerns are:
Perhaps we can soften the boilerplate blow by eliminating the need to register components to specify their storage type as part of this change; which would allow that information to be specified much more locally. This change may also enable interesting features like "immutable components" down the line, or soften the cost of requiring Clone (#1515) on components. I'm personally in favor of this, but it's a change that should be made deliberately. This is a prime candidate for an RFC once #1662 has landed :) |
I am also in favour of this. My 'planned' solution would:
trait Component {
fn maybe_register_as_reflect(...)->Option<...>;
fn storage_type() ->Option<StorageType> {None}
}
|
We should consider whether we want to do this for Resources as well. This would enable a proper solution to #1891 for instance, which would be very useful for the editor. |
In my mind, the default state is applying it for resources. We'd have to have some very good reasons not to. |
In my designs, I'm finding I would also like opt-in an opt-in |
@BoxyUwU points out that if we have this we should be able to make change detection tracking more easily opt-in or opt-out, resulting in significant memory savings. |
Is there any specific reason to not just to the super easy implementation where This also allows us to add onto the implementation in the future. |
I would prefer to wait until we:
We haven't seen the Bundle vs Component confusion as much since the 0.5 release (thanks to api renames), so I don't feel the pressure to "solve" that problem as much as I did before. And adding boilerplate without a solid "now you can do this" feels like a great way to annoy people. I don't want to just dive in and break peoples' code before we've built a full and compelling "story" around this. |
That's all fair! I was mostly just poking the bear to see where we were standing on this and the best way to proceed forward. |
For completions sake, I might add that personally I tripped over the insert(bundle) issue a number of times. |
One of my first uses of components I stumbled into this. It wasn't in the context of Bundles, but instead: And inserting this into an entity by accident with: There was no compile or runtime error, I was just left wondering why my Queries for Position weren't returning any results. |
I also like this for its self documenting implications. I sometimes have some utility structs and whatnot near where I define my components. This change would make it 100% clear what is a component and what is not, at a glance. |
There are many positives from adding I also like the "self-documenting" argument for this, and I see it being a correctness argument as well. Apart from nice marker trait that does all of the above, we can also use this derive for more things. One particularily attractive example (previously mentioned in this thread) is the storage type. By making storages an associated type, we make them known statically at compile time. This is a huge deal for iteration speed - right now we generate a lot of code and have some extra branches for every single query iterator. Being able to optimize those out through static typing is likely to increase performance significantly, as well as make the iterator implementation way less fragile to code changes (we are at the limit of compiler right now and already inline a lot of complicated code manually because of that). This would also allow for easier experimentation with different storage implementations without impacting the performance of existing code. The above argument might seems like a bad idea for dynamic components in the future, but I think it's not really the case. There is nothing wrong with having a There is also reflection or other features necessary for good editor experience. There is not much hope in implementing that without some kind of derive macro. The next best thing is global code analysis, but it is just a terrible idea. |
Implementation of opt-in |
Another idea from @BoxyUwU on this note to generalize the registration of events: trait [Component | Resource]: Send + Sync + 'static {
pub fn on_register(world: &mut World) {}
} I've needed this pattern in my Styles and Hooks proposals, and I would like to have a clean mechanism. Note that we'd want to solve the event cleanup issues with fixed time steps in one way or another to do this. This may also spare us from needing to call |
At this point you probably want I like the idea of registration hook though. Still, the details would have to be considered in context of #[derive(Component)]
#[on_register(my_register_fn)]
struct MyComponent { .. }
fn my_register_fn(world: &mut World) {
// ...
} I wonder if we can make it more natural, especially if we will add more hooks. |
@TheRawMeatball wants an Event trait already for #2073 to configure event storage :) |
An easy registration hook would be one of the major advantages of this design imo, as this means we can use the existing implicit registration infrastructure for more things, such as Reflect registration and possibly more. |
I think we should seriously consider "real trait impls" over building a complicated derive(Component) dsl. Ex: trait Component {
fn on_register(world: &mut World) {
}
}
#[derive(Component)]
struct A;
struct B;
// identical to A impl
impl Component for B {
type Storage = Table; // associated type defaults aren't stable yet :(
}
// once associated type defaults stabilize it would just be this
impl Component for B {}
struct C;
impl Component for C {
type Storage = Table;
fn on_register(world: &mut World) {
// ...
}
} But we should enumerate the scenarios we want to cover before picking a path. |
I'm trying to figure out the best way to implement the storage type declaration DSL. #[derive(Component)]
#[storage(sparse)]
struct A; I don't like that though, because every single extra declaration will have to be another attribute. Also there is no clear connection between attribute and the Component derive. So, I'm thinking about doing similar syntax to what serde does, and use single meta attribute with multiple "named parameters", like so #[derive(Component)]
#[component(storage = SparseSet)]
struct A; That would allow us to later add other common parameters if needed. Also the manual trait impl must be easy. My plan is to use associated impl Component for B {
const STORAGE: StorageType = StorageType::SparseSet;
} Alternatively an associated type could be used (this is in the current impl), but does require importing extra types that doesn't directly correspond to the parameters used in the attribute. impl Component for B {
type Storage = bevy::component::SparseSetStorage;
} This opens up the possibility of maybe allowing custom storage impls, but right now it's not really possible due to high level of coupling betweeen storages and ecs internals (those are basically one and the same right now). So for now the |
…#2254) This implements the most minimal variant of #1843 - a derive for marker trait. This is a prerequisite to more complicated features like statically defined storage type or opt-out component reflection. In order to make component struct's purpose explicit and avoid misuse, it must be annotated with `#[derive(Component)]` (manual impl is discouraged for compatibility). Right now this is just a marker trait, but in the future it might be expanded. Making this change early allows us to make further changes later without breaking backward compatibility for derive macro users. This already prevents a lot of issues, like using bundles in `insert` calls. Primitive types are no longer valid components as well. This can be easily worked around by adding newtype wrappers and deriving `Component` for them. One funny example of prevented bad code (from our own tests) is when an newtype struct or enum variant is used. Previously, it was possible to write `insert(Newtype)` instead of `insert(Newtype(value))`. That code compiled, because function pointers (in this case newtype struct constructor) implement `Send + Sync + 'static`, so we allowed them to be used as components. This is no longer the case and such invalid code will trigger a compile error. Co-authored-by: = <=> Co-authored-by: TheRawMeatball <[email protected]> Co-authored-by: Carter Anderson <[email protected]>
Solved in #2254. |
What problem does this solve or what need does it fill?
Currently,
Component
is auto implemented for any type that isSend + Sync + 'static
. This has some benefits:But there are also some downsides:
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.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 aderive
macro:The trait impl it expands to is trivial:
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 aComponent
as aBundle
(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:
I didn't require
<T>
to be a component in this case: by wrappingT
inMyGenericComponent
, we've declared our intent to make it aComponent
. The orphan rule ensures that only the owner ofMyGenericComponent<T>
can implementComponent
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.The text was updated successfully, but these errors were encountered: