Open bushrat011899 opened 2 weeks ago
The behavior of implementing Component
before this change and the behavior of implementing Component + ComponentMut
after this change should be identical. Do I understand that correctly?
Yes a pre-this-PR Component
is identical to a this-PR Component + ComponentMut
. Component
contains all the implementation details it had previously, but now only implies an immutable type. Mutability is now explicitly stated by implementing ComponentMut
. But for the derive macro, Component + ComponentMut
are implemented by default (since that is the most typical use-case). To opt-out of mutability in the derive macro, you add #[immutable]
.
Small nit: I would prefer #[component(immutable)]
to keep all component attributes together. It also follows #[world_query(mutable)]
.
I've updated the macro to instead use #[component(immutable)]
. It's much clearer what's happening and should be cleaner too. Good suggestion @ItsDoot.
Of note, FilteredEntityMut::get_mut_by_id
is (so far) the only safe method I have found that can bypass immutable components. I did want to add the immutable flag to ComponentDescriptor
, but propagating that information proved very challenging. If anyone has a suggestion for how to integrate ComponentMut
and ComponentDescriptor
in the least impactful way I would be greatly appreciative.
Why do you prefer the ComponentMut: Component
design over a Mutable + Component
design? I have a mild preference for the latter because I think it'll be easier to extend to resources. Broadly happy with this otherwise though, although I do think the reflection and dynamic component stories should probably be improved 🤔
Don't have time to look over this fully, but I like this. I also prefer the version without the trait bound on component.
Just so I am sure this can be used as I want, if we make parent/children immutable, how do we preserve the existing hierarchy commands api? Will we use unsafe within the commands to get mutable access, or go properly immutable with only clones and inserts?
Just so I am sure this can be used as I want, if we make parent/children immutable, how do we preserve the existing hierarchy commands api? Will we use unsafe within the commands to get mutable access, or go properly immutable with only clones and inserts?
I was wondering if it would make sense to have mutable component access require a key type. Then crates could keep that type private to simulate immutability while still being able to mutate the component themselves.
Not sure if that's possible and I don't know how well it fits with this approach, but possibly an option (though I’m going to guess far more complex and involved).
or go properly immutable with only clones and inserts
It would be this. Either through Parent
's on insert hook or a command.
We'll have to make Children
immutable too, won't we? That will mean some extra vec cloning, so might want to look at an immutable vector impl. But I'm content with that answer. We can deal with the costs if it turns out to be a problem.
Why do you prefer the
ComponentMut: Component
design over aMutable + Component
design? I have a mild preference for the latter because I think it'll be easier to extend to resources.
The three reasons for me were:
ReflectComponentMutable
ComponentMut
than Component + Mutable
Resource
and Component
, which would create a conflict on the common Mutable
It's probably fine to keep this component-only for now, considering that resources don't currently support hooks (so the applications are pretty minimal).
Eventually it seems like resources are going to become components anyway.
We'll have to make
Children
immutable too, won't we? That will mean some extra vec cloning, so might want to look at an immutable vector impl. But I'm content with that answer. We can deal with the costs if it turns out to be a problem.
This shouldn't be required. The data is only immutable while it is stored on the entity, so remove/mutate/insert can be used to avoid any cloning. If that pattern is cumbersome we could add a component_scope
method like resource_scope
, except with ownership of the component.
This shouldn't be required. The data is only immutable while it is stored on the entity, so remove/mutate/insert can be used to avoid any cloning. If that pattern is cumbersome we could add a component_scope method like resource_scope, except with ownership of the component.
Wouldn't this create unnecessary archetype moves? I feel like cloning is probably faster (in this case at least). But that's getting off topic.
Perhaps, then we'd do an Indiana Jones-style replacement where you temporarily place an empty/placeholder value while you're mutating the component. Basically std::mem::swap
but ensuring hooks are still triggered.
But agreed, this is an optimisation question we can improve.
You could even do a mutate_via_reinsertion
API, which could work on immutable components. Very clean, and no archetype moves. Easy followup though.
What happens if you ask for a &mut C
on an immutable C in a system? Is it the compile error where your function doesn't implement system?
Compiler error. The QueryData
implementation is not provided for immutable components.
There's some discussion on Discord around the name immutable. These components are mutable, but only when outside of the ECS. Once stored on an entity they become immutable. To clarify this, an alternative name is being bikeshedded. My personal preference is for frozen, as that has the same implications as immutable, with the added possibility of "thawing" (remove to mutate)
For posterity, frozen gets my vote as well, but "restricted components" is a safe fallback.
I'm a strong proponent of "immutable". It's the right level of technical precision for the users who will know they want to use this, and those who are unfamiliar will mostly hit error messages which we can make more friendly. But I'm happy to be overruled.
My objection to "immutable" is that many of the most useful operations involve indirectly mutating the data involved. Which is... confusing. It's also a lot harder to Google. I do appreciate the crisp technical correctness from FP here though. We'll see what the other SMEs want; I don't think either choice is bad.
I'd love if there was a trait like ComponentNonMut
. bevy_mod_index
would want to use that as a bound for components that can be reliability tracked with hooks now that mutations can be prevented.
We could maybe add an unsafe trait ComponentNonMut: Component { }
to cover that usecase. We would then have to make ComponentMut
unsafe too, since they're mutually exclusive. The derive macro could hide these details tho.
Unfortunately Rust's trait system doesn't play very nice with that sort of negative bound. If we had both MutableComponent
and ImmutableComponent
as traits, there's nothing stopping users from implementing both. I agree that both forms would be useful though.
Would an associated type work here?
impl <C: Component<Mutable=true>> Index {}
seems pretty workable, and I don't think it's much worse / less clear than the current design. It would also bypass the nasty reflection problems of the current approach.
Using an associated type on Component
allows resolving the reflection DX issues previously introduced by this PR. Additionally, we can now offer a marker trait ComponentImmutable
which is effectively !ComponentMut
.
After further iteration, the ComponentMut
and ComponentImmutable
traits have been removed, opting instead for matching against the associated type Mutability
.
/// Work with a component, regardless of its mutability
fn get_component<C: Component>(/* ... */) { /* ... */ }
/// _Only_ allow mutable components
fn get_component_mut<C: Component<Mutability = Mutable>>(/* ... */) { /* ... */ }
/// _Only_ allow immutable components
fn get_component_immutable<C: Component<Mutability = Immutable>>(/* ... */) { /* ... */ }
If you find this cumbersome, you can easily create your own blanket-impl
trait(s):
pub trait ComponentMut: Component<Mutability = Mutable> {}
impl<C: Component<Mutability = Mutable>> ComponentMut for C {}
pub trait ComponentNonMut: Component<Mutability = Immutable> {}
impl<C: Component<Mutability = Immutable>> ComponentNonMut for C {}
I think that my only remaining major request is that this has a test suite for dynamic components. Once that's done I'll do a final polish pass on this at the start of 0.16.
I really like how this PR is turning out. I'm a fan of the associated type idea.
I have added a second example, immutable_components_dynamic
, which demonstrates creating dynamic immutable components at runtime. In particular, the example shows that while get_by_id(...)
will succeed for an immutable component, get_mut_by_id(...)
will return an Err
, since that component cannot be mutably accessed.
There is definitely room for better documentation, and there's probably some additional methods/changes to existing methods we'd want to do before merging, but I think I have sufficiently covered the bulk of the work for this feature. I look forward to more detailed feedback in the coming weeks!
As an aside, I added #[component(immutable)]
to Name
to see if anything broke, and to my pleasant surprise it just worked without any knock-on effects. Bevy already treats Name
as some immutable data, so making it "official" is a 1-line PR once this is merged. I haven't included that in this PR just to keep the scope as small as it can be.
The associated trait magic with sealed logic types is very cute, I like it. Is the issue with FilteredEntityMut::get_mut_by_id
resolved or still outstanding?
The associated trait magic with sealed logic types is very cute, I like it. Is the issue with
FilteredEntityMut::get_mut_by_id
resolved or still outstanding?
Thanks! And yes that issue is resolved. The move to an associated type made enough information available on Component
for me to have immutability be a bool
on ComponentDescriptor
.
Objective
Solution
Component
,Mutability
, which flags whether a component is mutable, or immutable. IfMutability= Mutable
, the component is mutable. IfMutability= Immutable
, the component is immutable.derive_component
to default to mutable unless an#[component(immutable)]
attribute is added.ReflectComponent
to check if a component is mutable and, if not, panic when attempting to mutate.Testing
immutable_components
example.Showcase
Users can now mark a component as
#[component(immutable)]
to prevent safe mutation of a component while it is attached to an entity:This prevents creating an exclusive reference to the component while it is attached to an entity. This is particularly powerful when combined with component hooks, as you can now fully track a component's value, ensuring whatever invariants you desire are upheld. Before this would be done my making a component private, and manually creating a
QueryData
implementation which only permitted read access.Using immutable components as an index
```rust /// This is an example of a component like [`Name`](bevy::prelude::Name), but immutable. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Component)] #[component( immutable, on_insert = on_insert_name, on_replace = on_replace_name, )] pub struct Name(pub &'static str); /// This index allows for O(1) lookups of an [`Entity`] by its [`Name`]. #[derive(Resource, Default)] struct NameIndex { name_to_entity: HashMapAdditionally, users can use
Component<Mutability = ...>
in trait bounds to enforce that a component is mutable or is immutable. When usingComponent
as a trait bound without specifyingMutability
, any component is applicable. However, methods which only work on mutable or immutable components are unavailable, since the compiler must be pessimistic about the type.Migration Guide
Component
manually, you must now provide a type forMutability
. The typeMutable
provides equivalent behaviour to earlier versions ofComponent
:Component<Mutability = Mutable>
rather thanComponent
if you require mutable access to said component.Mut<T>
will now typically return anOccupiedEntry<T>
instead, requiring you to add aninto_mut()
to get theMut<T>
item again.Notes
I've done my best to implement this feature, but I'm not happy with how reflection has turned out. If any reflection SMEs know a way to improve this situation I'd greatly appreciate it.There is an outstanding issue around the fallibility of mutable methods onReflectComponent
, but the DX is largely unchanged frommain
now.Component<Mutability = Mutable>
, but there may still be some methods I have missed. Please indicate so and I will address them, as they are bugs.