jakobhellermann/bevy_mod_js_scripting

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:

  1. 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?
  2. 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 the ReflectComponent (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 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? My bad, I imported 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.