-
-
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
Dynamic Component Id type #12794
Dynamic Component Id type #12794
Conversation
note: name needs work - added TypedComponentId - added world_id() to all entity cells - added get_dynamic(), get_dynamic_mut(), into_dynamic_mut(), insert_dynamic() as appropriate to entity cells - added init_dynamic_component() to World - added new_dynamic() to ComponentDescriptor
Worth considering, in my prototype I restrict |
At least for me one of the key benefits of dynamic components is that they don't require newtyping and new variants can be made on the fly, hence the lack of a Component constraint. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The approach looks good to me. Some things that I would also like to see:
- some tests, both for the happy path and for when the
component_id
refers to a different world or a component that doesn't exist; - some examples on how to use this together with dynamic queries (it would be very nice if we could remove the
unsafe
from theecs/dynamic
example); - an API that works with untyped
ComponentId
s too (right now you have to carryT
everywhere, which might not be wanted in some cases):- for example this could be done with a method that upgrades a
ComponentId
to aTypedComponentId<T>
by checking if the registeredTypeId
isT
's.
- for example this could be done with a method that upgrades a
pub fn init_dynamic_component<T: Send + Sync + 'static>( | ||
&mut self, | ||
storage_type: StorageType, | ||
) -> TypedComponentId<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you could have a init_dynamic_component
that takes a T: Component
and a init_dynamic_component_with_storage
that only takes T: Send + Sync + 'static
but requires specifying the storage?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the first just sounds like init_component. Should that just return the typed wrapper then, since the inner id is still accessible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing init_component
to return TypedComponent<T>
might be controversial, although consistent with this change.
if self.world_id() != component_id.world_id { | ||
panic!("Attempted to insert a dynamic component into the wrong World"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to be sure I would put a debug_assert!
after all the world id checks that verifies that the type id of T
is the same as the one in the ComponentDescriptor
associated to component_id
in this World
.
debug_assert_eq!(self.components().get_info(component_id.id).and_then(|d| d.type_id()), Some(TypeId::of::<T>()));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good idea.
pub struct TypedComponentId<T: Send + Sync + 'static> { | ||
pub(crate) id: ComponentId, //todo: pub(crate) fields and have the World construct this, or private fields and construct from an &mut World reference? | ||
pub(crate) world_id: WorldId, | ||
pub(crate) data: PhantomData<T>, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should document that a safety invariant of this type is that the world with id self.world_id
contains a component with id self.id
of type T
.
I wonder how much more ergonomic that is though, since you have to carry the Also note that you won't need a newtype for each dynamic component. With a single newtype around e.g. a |
Taking that to the extreme, you would probably just end up with TableStorageComponent<> and SparseStorageComponent<> newtypes, which should probably just end up as parameters anyway. Unless I'm misunderstanding you (good chance of that tbh). In my experience dynamic components do require the id being passed around, but I don't see a good way to avoid that. Edit: just saw your "upgrading" idea. Seems useful, but I don't think it's a full replacement since you'd still have to pass the id around, or use ComponentIdOf, which will ignore any dynamic components. |
todo:
|
What is the expected use case here? Untyped components were originally introduced as a way to enable scripting interfaces, but here you have a Rust type. Can you provide a end-user or library use case that requires what are effectively runtime-generated newtype components? |
For example if you're implementing scripting using an interpreted language this would let you store multiple components that hold a |
I feel in many use cases for dynamic components we actually do have a known type we want to treat the component as when we access it. As mentioned above many scripting use cases will be defining many of it's components as a shared |
My question for this implementation is if we have to check the world id anyway to ensure safety would it be worth it to drop the type wrapper all together and just check whether the type id matches the one you registered it with? That way you can just use component ids everywhere. |
The author linked this to me when I was asking on the Discord for having dynamic marker types. And I am sold on this as a solution for what I want to do. My library offers a few SystemParams with non-pub Queries in them. Part of the logic requires marker components. But it is impossible for such a component to be unique to every usage of my SystemParam. So I want to use this feature here to newtype It is still not easy for the SystemParam to learn what the ComponentId of the marker type ended up being. But when my SystemParam initializes it's params, my marker type will have put it's ComponentId in a resource, discoverable by the following logic of the SystemParams init. Once the param knows the ComponentId, it can create Commands that adds and removes them. Still a bit hacky, probably not what the most obvious use case could be here, but a use case only possible with this. |
FYI, I ended up publishing my MVP as |
closing, was intended to solve a very niche issue that doesn't exist anymore (at least for me) |
Objective
Currently working with dynamic component ids requires a lot of manual pointer work and unsafe code, which seems unnecessary when dynamic components are registered from known rust code.
Solution
Add a wrapper around ComponentId with a type parameter T to act as a witness that that id corresponds to a component with type T. This allows registering multiple components with the same underlying type, that dynamic queries can access separately in a safe way.
Changelog