What is still missing for bevy and/or this repo to declare new component entirely in dynamic fashion?
Escapingbug opened this issue · 8 comments
It seems now with bevy_reflect
we are here for scripting in bevy, finally! Currently I am on a project that requires a dynamic updateable ECS support, and I am doing research on bevy-ecs and if we could get there.
I already see how insert into the ECS world can be done once we have the component defined even as an empty struct to provide the reflected Type in Rust. But the thing is it looks to me that we still need more functionality in bevy to bypass the lacking of type id (as well as TypeRegistration
problem).
I saw you guys also exploring this in here. It seems to me that if we are gonna emulate the type id with our own defined type and let JS defined components share this type, we might lose some parallelism?
Since I still haven't explored the whole parallel part of Bevy's implementation, I am just imgining this. For example, if system A requires component A and system B requires component B but both are in JS, will this two systems possible to be run in parallel?
And, if this really is a problem, what can we do to the Bevy or do we have any workaround to solve this?
Hey there!
We are actually quite close to being able to define new components, I think, with the PR you found being the only thing I know so far that's blocking it.
Type IDs
Regarding type IDs and registration, I initially had similar thoughts to you: questions about how we can define components without type IDs, but after looking more into it, we don't need to.
When we define a component in JavaScript, we need some way to represent that component in the Bevy ECS, so that it can be stored in the ECS world, even if that is simply a raw pointer, or an array of bytes. That raw pointer or array of bytes, has a TypeId
, so even though the component is defined in JavaScript, there is a Rust type that is responsible for storing our component, and that is the TypeId
of our component, and also the type that we need to insert into the TypeRegistry
.
Additionally, it needs to implement Reflect
, so it will probably be a custom type that we make similar to a serde_json::Value
for storing any JavaScript type.
Then what we need is a different component ID, which can be accomplished with World::init_component_with_descriptor
. Each time we init a component with a descriptor, we get a new component ID. So we can register any number of different JS components with the same backing Rust type, that are indeed different components as far as Bevy is concerned.
Parallelism
As far as parallelism, that's not currently a feature in bevy_mod_js_scripting
. For simplicity, all JavaScript scripts have access to the entire Bevy world, and therefore have to run one at a time.
In the future, we could potentially have a way to configure JavaScript systems with pre-defined system parameters, just like Bevy systems, and we could run parallel JavaScript runtimes to actually get multi-threaded execution of JS. Essentially it would work the same as normal Bevy systems, where they aren't allowed to borrow the same components at the same time, and Bevy would attempt to run them in parallel. It wouldn't be any different with built-in components, or scripted components.
That's much more complicated, though, and I think it's more important for us to get the simpler, single-threaded scenario working solidly first.
Single threaded scripting should be totally sufficient for tons of different use-cases, still.
Thanks for all the details! I think I got this now. I'm closing this and waiting for the prototype! I'll see what I can do to help when I got time for this.
Sorry to bother a little bit further. I'm now working on some private project which need to implement this. I understand what is mentioned in this issue but now having trouble understanding the relationship between custom ReflectComponent
and inserting the component into the world. I know we can get the component id, but how the component id could end up inserting the component into the world confuses me.
It would be really nice of you if you could help me understanding this part since I really don't know where to get help. @zicklag
Yeah, I'd be glad to help explain.
Fair warning, I haven't tested the custom component thing yet, so this is my current idea of how it should probably be able to work.
So, the scenario is that we want our JavaScript scripts to be able to create new components.
The first thing we have to do is have a way to store the component data. We'll create a Rust struct looks like this:
#[derive(Reflect)]
struct JsComponent {
data: serde_json::Value,
id: ComponentId,
}
First lets understand the data
field, which is a serde_json::Value
. The idea here is to allow scripts to store any JSON value in custom JS components. Since serde_json
already comes with a Value
type to represent JSON, we just use that.
Now we need to understand the id
field. To do that, let's consider a scenario:
We have a script that needs to create two kinds of components, a Position
component, and a Velocity
component.
We'd hypothetically do something like this in JavaScript:
const positionComponentId = world.registerComponent();
const velocityComponentId = world.registerComponent();
Each time we call world.registerComponent()
in JavaScript, it would go and trigger Rust code that would register a new component type like this:
let new_component_id = world.init_component_with_descriptor(ComponentDescriptor::new::<JsComponent>());
That new_component_id
would be the only thing that would make a distinction between all of the different JavaScript-defined components, because they all store their data in the same JsComponent
Rust struct.
So, let's say we need to create a new Velocity
component from JS, we could do it like this:
const velocity = Value.createJsComponent(velocityComponentId, { x: 30, y: -9.8 });
In Rust land, this would create a new JsComponent
struct with the id
set to velocityComponentId
and the data
set to { x: 30, y: -9.8 }
as a serde_json::Value
.
Then it would get wrapped in a Box<dyn Reflect>
before getting passed back to JavaScript as a value ref.
We now have a JS variable called velocity
that is storing a Box<dyn Reflect>
that is actually a JsComponent
storing our component data.
Now comes the time when we want to add this velocity component to an entity.
We should be able to do this in a script in the same way that scripts can already add Rust-defined components:
world.insert(entity, velocity);
But, it is important to realize here that the velocity
variable holds a Box<dyn Reflect>
so to add it as a component to an entity, we need a ReflectComponent
implementation, that we can get from the TypeRegistry
( I'm assuming you understand the type registry, let me know if you need me to explain that more ).
The default implementation of ReflectComponent
, that you can get on structs if you #[reflect(Component)]
will add the component using a component ID that is determined from the TypeId
of the struct.
For instance if I did this in Rust:
#[derive(Reflect)]
#[reflect(Component)]
struct MyComponent;
The ReflectComponent
implementation would insert any MyComponent
structs with the same ComponentId
as every other MyComponent
struct.
In our case, though, each JsComponent
struct we have, may actually have a different ComponentId
that it needs to be inserted with.
So, what we need is a custom implementation of ReflectComponent
that, when inserting the component, will use the ComponentId
from the id
field of the JsComponent
struct, instead of inserting it with the same ComponentId
as every other JsComponent
, like the default implementation would.
This allows us to have any number of JavaScript-defined components, but to store them all in 1 Rust struct, and have a way to know which ComponentId
to insert on the component, when adding it to an entity.
@zicklag
I think I now understand the most part of the explaination. But there are still several issues confuse me a bit.
As I understand, the purpose of getting a ReflectComponent
is that we want to call what should be provided by the derived Component
but we can't since we have no explicit type (only dyn Reflect
). Then we get a ReflectComponent
and do that insert by calling the insert on ReflectComponent
given the dyn Reflect
. By that, the whole procedure does not require the explicit type of the dyn Reflect
. The question is, when we do a custom implementation of ReflectComponent
, we are like writing our own implementation of Component
and when insert things, we insert according to our own ComponentId
instead of the ComponentId
associated with the TypeId
.
I've looked the implementation of ReflectComponent
's insertion (here if I'm not reading the wrong location). And the insertion implementation indicates that if we want to replace it, we might want to replace the insertion so that when we insert, we do not rely on world.entity_mut(entity).insert()
(since it uses the type id related component id) we have to somehow insert the component given the component id to the world.
Now my question is:
- How can we achieve that? Given the component id, how to insert the component to the entity? I read the insertion part and it seems some of the functions care about the component id are not public (insert API has changed, a new bundle inserter mechanism is now used, and the insert is by first fetching the
BundleInfo
then get the inserter to insert), we might need to simulate this insertion process? - And, if we really did the simulation, why do we need the
ReflectComponent
after all since we can just insert our dynamic omponent (JsComponent
) with reference to its internal component id? Why not just use the custom insertion procedure and forget about theReflectComponent
(since it is just a collection of procedures)?
Another problem is, I don't know if it is because my incorrect configuration or what, the plain example of reflected traits example here in the bevy_reflect fails when fetching My bad, I imported ReflectDoNothing
on TypeRegistry
. A None
is returned somehow (I'm almost just copy-pasting the whole piece of code). Is there anything more I should do to make this work?std::any::TypeId
which conflicts with default type_id()
implementation.
AFAIK, current insertion is done by getting the BundleInfo
(which consumes the component ID, now by the component_id()
attached to Bundle
trait), and does the insertion after that.
If we are gonna somehow intercept the component_id
, we might have to need to replicate the logic within the BundleInfo
creation but use our own component id?
If we do that, do we still need to use the ReflectComponent
?
Also, I found this PR implements the insertion. So, do we also need to wait for this to be merged?
Since according to my investigation, I think there are possibilities we still need things from bevy to cope with the dynamic component definition problem. This issue is reopened then.
If we do that, do we still need to use the ReflectComponent?
We still need ReflectComponent
, but only if we want seamless support for our JsComponent
in bevy_mod_js_scripting
and any other similar projects.
If we wanted to go the route of not using ReflectComponent
, that could work, but when inserting components in bevy_mod_js_scripting
, we still only have a Box<dyn Reflect>
so we don't know when we're inserting a JsComponent
. We'd have to try downcasting each Box<dyn Reflect>
to a JsComponent
before attempting to insert it, so that we could know whether or not we should use our custom insertion procedure.
So by using ReflectComponent
, we make the whole custom insertion process transparent to most of bevy_mod_js_scripting
, so that it can be handled the same as any other component.
As far as component insertion, I think technically we can implement insertion by implementing Bundle
, which, interestingly, has new docs in Bevy saying that it's unsupported and must not be done.
I think we'll need to open a discussion in Bevy about how we are supposed to insert and remove components with custom component IDs.
I'll open a discussion topic.
Opened a discussion here: bevyengine/bevy#6536.
Edit:
Also, I found bevyengine/bevy#5602 implements the insertion. So, do we also need to wait for this to be merged?
Good find! I missed your link there and it was linked to in the discussion I opened. That would indeed work, we just need a way to delete components given their ComponentId
, now.