bevy_mod_scripting
first and foremost relies on Reflection
, a feature of Bevy which allows us to interact with type erased data. This is the foundation of the scripting system, as it allows us to interact with the Bevy ECS without knowing the exact types of the components/resources our scripts will be interacting with at compile time.
Normally in Bevy, you would define your components and resources as structs, and then use them in your systems. This is very powerful but also very limiting, as it requires you to know the exact types of the components/resources you will be interacting with at compile time. This is where Reflection
comes in.
Bevy provides us with a TypeRegistry
, which is essentially just a map from type ids to TypeRegistrations
. A TypeRegistration
is a container for all sorts of metadata about the type but most importantly it allows us to query TypeData
of any type which was previously registered via the TypeRegistry
.
How is this useful ? Well it allows us to register arbitrary information including function pointers which we can then retrieve given just a TypeId
. This is exactly what we do with ReflectProxyable
, the interface between Bevy and Lua:
pub fn ref_to_lua<'lua>(
&self,
ref_: ReflectReference,
lua: &'lua Lua
) -> Result<Value<'lua>, Error>
pub fn apply_lua<'lua>(
&self,
ref_: &mut ReflectReference,
lua: &'lua Lua,
new_val: Value<'lua>
) -> Result<(), Error>
A ReflectProxyable
TypeData
is registered for every type which we want to have custom Lua bindings for. With this we can represent any Reflectable type in any way we want in Lua. For example we can represent a Vec3
as a table with x
, y
, z
fields, or we can represent it as a userdata with a metatable which has __index
and __newindex
metamethods. The best part about this is we do not need to even own the types we are adding this TypeData
for! This bypasses the pesky orphan rule and allows us to add custom Lua bindings for any type in Bevy.
Note: for your own types you can do this by deriving Reflect
and adding a reflect(LuaProxyable)
attribute like so:
#[derive(Reflect)]
#[reflect(LuaProxyable)]
pub struct MyType {
pub x: f32,
pub y: f32,
pub z: f32,
}
impl LuaProxyable for MyType {
// ...
}
Now when you register your type with the AppTypeRegistry
it will automatically have a ReflectLuaProxyable
TypeData
registered for it! You must not forget to register your type with:
app.register_type::<MyType>();
All accesses to the bevy world are done via ReflectReference
types which look like this:
pub struct ReflectReference {
/// The reflection path from the root
pub(crate) path: ReflectionPath,
pub(crate) world_ptr: WorldPointer,
}
I.e. they are essentially just a path to the data in the Bevy world. This allows us to have a reference to a piece of data in the Bevy world which can be passed around and modified in Lua safely.
The ReflectionPath
itself consists of a "base" reference and a list of path segments. Most interesting of which is the base:
pub(crate) enum ReflectBase {
/// A bevy component reference
Component {
comp: ReflectComponent,
entity: Entity,
},
/// A bevy resource reference
Resource { res: ReflectResource },
/// A script owned reflect type (for example a vector constructed in lua)
ScriptOwned { val: Weak<RwLock<dyn Reflect>> },
}
Given a valid base and a valid path we should always be able to get a valid reference to a piece of data in the Bevy world. Note we make use of other TypeData
here, i.e. ReflectComponent
and ReflectResource
which store function pointers for the specific types of components/resources we are dealing with that allow us to interact with them. For example ReflectComponent
let's us call:
pub fn reflect<'a>(
&self,
entity: EntityRef<'a>
) -> Option<&'a (dyn Reflect + 'static)>
To retrieve a reflect reference to our component on a specific entity!
You might be wondering how exactly we get a TypeId
from a script in the first place, and the answer is we use a simple String type name! The journey begins in our custom World
UserData:
methods.add_method("get_type_by_name", |_, world, type_name: String| {
let w = world.read();
let registry: &AppTypeRegistry = w.get_resource().unwrap();
let registry = registry.read();
Ok(registry
.get_with_short_type_path(&type_name)
.or_else(|| registry.get_with_type_path(&type_name))
.map(|registration| LuaTypeRegistration::new(Arc::new(registration.clone()))))
});
Given a String type name like: my_crate::MyType
we can then retrieve both TypeId
and TypeRegistration
structs, which we can use to retrieve any TypeData
we need!
Now finally our ReflectReference
type has a custom IntoLua
implementation which does the following:
- Check the type has a
ReflectLuaProxyable
TypeData
- If it does, call
ref_to_lua
on it and generate the Lua representation of the data - If it does not, default to a "vanilla" representation of the data i.e. a
ReflectedValue
which is a simple wrapper around aReflectReference
. It uses pure reflection to provide__index
and__newindex
metamethods for the data.
fn into_lua(self, ctx: &'lua Lua) -> mlua::Result<Value<'lua>> {
let world = self.world_ptr.clone();
let world = world.read();
let typedata = &world.resource::<AppTypeRegistry>();
let g = typedata.read();
let type_id = self.get(|s| s.type_id())?;
if let Some(v) = g.get_type_data::<ReflectLuaProxyable>(type_id) {
v.ref_to_lua(self, ctx)
} else {
ReflectedValue { ref_: self }.into_lua(ctx)
}
}
Note that assigning to bevy via ReflectedValue's will check if the value we're trying to assign has a ReflectLuaProxyable
type data, and if it does it uses it's apply_lua
method to apply the new value to the ReflectReference
, if it does not it expects it to be another ReflectedValue
and will clone then apply it to itself using pure reflection.
All primitive data types will have a ReflectLuaProxyable
type data registered for them via their FromLua
and Clone
implementations.
We provide a set of macros to make it easier to define custom Lua bindings for your types. For example:
#[derive(LuaProxy, Reflect, Resource, Default, Debug, Clone)]
#[reflect(Resource, LuaProxyable)]
#[proxy(
derive(clone),
functions[
r#"
#[lua(kind="MutatingMethod")]
fn set_my_string(&mut self, another_string: Option<String>);
"#,
r#"
#[lua(kind="MutatingMethod")]
fn set_with_another(&mut self, #[proxy] another: Self);
"#,
r#"
#[lua(kind="Method")]
fn get_my_string(&self) -> String;
"#,
r#"
#[lua(kind="Method",raw)]
fn raw_method(&self, ctx : &Lua) -> Result<String, _> {
let a = ctx.globals().get::<_,String>("world").unwrap();
let a = self.inner()?;
Ok("".to_owned())
}
"#,
r#"
#[lua(kind="MetaMethod", metamethod="ToString")]
fn to_string(&self) -> String {
format!("{:#?}", _self)
}
"#
])
]
pub struct MyProxiedStruct {
my_string: String,
}
will generate a LuaMyProxiedStruct
which will act as the Lua representation of MyProxiedStruct
. It will have the following methods:
set_my_string
which will set themy_string
field of the structset_with_another
which will set the struct to be equal to another structget_my_string
which will return themy_string
field of the structToString
metamethod which will return a string representation of the struct
It will also implement UserData
for the proxy, meaning it can be passed around in Lua as a first class citizen. And it will implement LuaProxyable
for MyProxiedStruct
, meaning you can register your type and have it work in Lua with no extra work!
A good scripting system should be able to interact with the Bevy API as well as the user's own types. We provide a way to generate Lua bindings for the Bevy API using a rustc plugin. We scrape the Bevy codebase and generate proxy macro invocations like the one above for every appropriate Reflect
implementing type, and package them in APIProvider
structs which you can use to provide the Bevy API to your Lua scripts.
This generator is a work in progress but it is designed with the possibility of generating bindings for ANY crate in mind. It is not limited to Bevy, and can be used to generate bindings for any crate which uses Reflect
types. In theory you should be able to use the CLI to generate your own bindings without writing macros yourself! See the bevy_api_gen
crate for more information.