Installation
Cargo
First you need to install the crate by adding this entry to your Cargo.toml
dependencies list:
bevy_mod_scripting = { version = "0.9.0", features = ["lua54"]}
Choose the language features you wish enabled and add them to the features block.
Bevy Plugin
The next step is to add the BMS plugin to your application, on top of any other extras you want included in your app:
app.add_plugins((
LuaScriptingPlugin::default(),
ScriptFunctionsPlugin
));
The above is how you'd setup BMS for Lua, if you want to use another language, simply use a corresponding plugin from the integration crate.
Language Features
Each language supported by BMS can be switched-on via feature flag as below:
Language | Feature Flag |
---|---|
Lua51 | lua51 |
Lua52 | lua54 |
Lua53 | lua53 |
Lua54 | lua54 |
Luajit | luajit |
Luajit52 | luajit52 |
Luau | luau |
Rhai | rhai |
Rune | rune |
Extra Features
In order to fit as many use cases as possible, BMS allows you to disable a lot of its functionality.
By default all of the useful features are enabled, but you may disable them if you wish if you are only needing BMS for script lifecycle management, and want to populate the bindings yourself.
Feature | Description |
---|---|
core_functions | If enabled, will enable all core functions, i.e. bevy integrations which let you interact with Bevy via reflection |
bevy_bindings | If enabled, populates the function registry with additiona automatically generated bevy bindings. This includes functions on glam and bevy::ecs types. These are useful but will slow down compilation considerably. |
mlua_async | Enables mlua/async |
mlua_serialize | Enables mlua/serialize |
mlua_macros | Enables mlua/macros |
unsafe_lua_modules | Allows loading unsafe modules via require in lua |
Managing Scripts
Scripts live in the standard bevy assets
directory. Loading a script means:
- Parsing the script body
- Creating or updating the resources which store script state
- Assigning a name/id to the script so it can be referred to by the rest of the application.
Loading
BMS listens to ScriptAsset
events and reacts accordingly. In order to load a script, all you need to do is request a handle to it via the asset server and store it somewhere.
Below is an example system which loads a script called assets/my_script.lua
and stores the handle in a local system parameter:
fn load_script(server: Res<AssetServer>, mut handle: Local<Handle<ScriptAsset>>) {
let handle_ = server.load::<ScriptAsset>("my_script.lua");
*handle = handle_;
}
In practice you will likely store this handle in a resource or component, when your load all the scripts necessary for your application.
Unloading
Scripts are automatically unloaded when the asset is dropped. This means that if you have a handle to a script and it goes out of scope, the script will be unloaded.
This will delete references to the script and remove any internal handles to the asset. You will also need to clean up any handles to the asset you hold in your application in order for the asset to be unloaded.
Hot-loading scripts
To enable hot-loading of assets, you need to enable the necessary bevy features as normal see the bevy cheatbook for instructions.
Assuming that hot-reloading is enabled for your app, any changes to script assets will automatically be picked up and the scripts re-loaded.
Manually (re)loading scripts
In order to manually re-load or load a script you can issue the CreateOrUpdateScript
command:
CreateOrUpdateScript::<LuaScriptingPlugin>::new("my_script.lua".into(), "print(\"hello world from new script body\")".into(), asset_handle)
replace LuaScriptingPlugin
with the scripting plugin you are using.
Manually Deleting scripts
In order to delete a previously loaded script, you will need to issue a DeleteScript
command like so:
DeleteScript::<LuaScriptingPlugin>::new("my_script.lua".into())
replace LuaScriptingPlugin
with the scripting plugin you are using.
Loading/Unloading timeframe
Scripts asset events are processed within the same frame they are issued. This means the moment an asset is loaded, it should be loaded and ready to use in the Update
schedule. Similarly, the moment an asset is deleted, it should be unloaded and no longer usable in the Update
schedule.
Attaching Scripts
Once you have scripts discovered and loaded, you'll want to run them.
At the moment BMS supports two methods of making scripts runnable:
- Attaching them to entities via
ScriptComponent
's - Adding static scripts
And then sending script event's which trigger callbacks on the scripts.
Attaching scripts to entities
In order to attach a script and make it runnable simply add a ScriptComponent
to an entity
commands.entity(my_entity).insert(ScriptComponent::new(vec!["my_script.lua", "my_other_script.lua"]));
When this script is run the entity
global will represent the entity the script is attached to. This allows you to interact with the entity in your script easilly.
Making static scripts runnable
Some scripts do not require attaching to an entity. You can run these scripts by loading them first as you would with any other script, then either adding them at app level via add_static_script
or by issuing a AddStaticScript
command like so:
commands.queue(AddStaticScript::new("my_static_script.lua"));
The script will then be run as any other script but without being attached to any entity. and as such the entity
global will always represent an invalid entity.
Note: Internally these scripts are attached to a dummy entity and as such you can think of them as being attached to an entity with an id of 0
.
Running Scripts
Scripts can run logic either when loaded or when triggered by an event. For example the script:
print("hello from load time")
function on_event(arg1)
print("hello from event time")
print(arg1)
end
Will print "hello from load time" when the script is loaded, and "hello from event time" when the script receives an event targeting the on_event
callback with a receiver list including this script or entity.
In order to trigger on_event
you need to first define a label, then send an event containing the label:
#[derive(Reflect)]
pub struct MyReflectType;
// define the label, you can define as many as you like here
callback_labels!(OnEvent => "on_event");
// trigger the event
fn send_event(mut writer: EventWriter<ScriptCallbackEvent>, mut allocator: ResMut<AppReflectAllocator>) {
let allocator = allocator.write();
let my_reflect_payload = ReflectReference::new_allocated(MyReflectType, &mut allocator);
writer.send(ScriptCallbackEvent::new_for_all(
OnEvent,
vec![my_reflect_payload.into()],
));
}
Note the second argument is the payload we are sending with the event, in this case we are sending an arbitrary reflect type MyReflectType
. This can be any type you like, as long as it implements Reflect
.
Other variants of the ScriptValue
enum are available for sending different types of data, such as ScriptValue::Integer
for primtive, types.
Event Handlers
In order for the events you send to actually be picked up, you need to inject special systems into your application. These systems will listen for the events and trigger the appropriate callbacks on the scripts:
app.add_systems(Update, event_handler::<OnEvent, LuaScriptingPlugin>);
Note the system is parameterized by the label we defined earlier, and the scripting plugin we are using. You can add as many of these systems as you like.
The event handler will catch all events with the label OnEvent
and trigger the on_event
callback on all targeted scripts which have that callback defined.
In order to handle events in the same frame and not accidentally have events "spill over" into the next frame, you should make sure to order any systems which produce these events before the event handler systems.
Controlling Script Bindings
In this book we refer to anything accessible by a script, which allows it to communicate with your Rust code a binding
(which in previous versions was more generically referred to as a script API).
The "binding" here being used as in: binding script
code to rust
code.
Namespaces
Namespaces are a way to group functions together, and are used to prevent naming conflicts. You can have multiple namespaces, and each namespace can have multiple functions.
Language implementations will also look for specific functions registered on your type first before looking at the generic ReflectReference
namespace.
Dynamic Functions
Everything callable by scripts must first be registered in the dynamic function registry. Notably we do not make use of the normal bevy function registry to improve performance and usability. This means you cannot call just any function.
In order for a function to be callable by a script it must adhere to a few requirements:
- Each argument must implement
FromScript
. - Each return type must implement
IntoScript
. - Each argument must also implement
GetTypeDependencies
- Each return type must also implement
GetTypeDependencies
The into/from requirements allow us to convert these types to ScriptValue
's, and each supported scripting language can then marshall these into the script.
Note these types are implemented for primitives, but if you want to interact with one of your Reflect
implementing types, you will need to use one of Ref<T>
, Mut<T>
or Val<T>
wrappers in place of &T
, &mut T
and T
respectively.
These wrappers enable us to safely interact with bevy, and claim any necessary mutex'es on Resources
, Components
or Allocations
.
The GetTypeDependencies
, trait is simply a local trait alias for GetTypeRegistration
with less strict type requirements. It allows us to register all the types necessary for the function calls, so that you don't have to register anything manually. If your type implements GetTypeRegistration
you should not face any issues on this front.
Registering Script Functions
Registering functions can be done via the NamespaceBuilder
like below:
NamespaceBuilder::<ReflectReference>::new(&mut world)
.register(
"hello_world",
|s: String| {
println!(s)
},
);
NamespaceBuilder::<GlobalNamespace>::new_unregistered(&mut world)
.register(
"hello_world2",
|s: String| {
println!(s)
},
);
This will allow you to call this function within lua like so:
some_type:hello_world("hi from method!");
hello_world2("hi from global!");
Note the new_unregistered
call instead of new
, this is because GlobalNamespace
is not a Reflect
type, and the new
call also automatically registers the type in the reflection registry.
Context Arguments
Each script function call always receives an additional context argument: FunctionCallContext
.
You can opt-in to receive this argument in your own function definitions by adding it as the first argument.
The context contains requests from the caller to your function, such as "I am calling you from a 1-indexed array system, please convert the index first", This argument is only relevant if you're targeting multiple languages.
It also allows you to retrieve the world via FunctionCallContext::world()
.
You can use this as follows:
NamespaceBuilder::<ReflectReference>::new(&mut world)
.register(
"hello_world",
|ctx: FunctionCallContext, s: String| {
let world = ctx.world()?;
let should_use_0_indexing = ctx.convert_to_0_indexed;
println!(should_use_0_indexing);
println!(s)
Ok(())
},
);
Generic Arguments
Sometimes you might want to be generic over the type of argument you're accepting, you can do so by accepting ScriptValue
arguments like so:
NamespaceBuilder::<ReflectReference>::new(&mut world)
.register(
"is_integer",
|s: ScriptValue| {
match s {
ScriptValue::Integer(i) => true,
_ => false
}
},
);
You can treat return values similarly.
Fallible functions
Your script functions can return errors either by:
- Returning
Result<T: IntoScript, InteropError>
- Returning
ScriptValue
and manually creating theScriptValue::Error(into_interop_erorr.into())
variant.
Reserved Functions
There are a few reserved functions that you can override by registering them on a specific type:
Function Name | Description | Overridable? | Has Default Implementation? |
---|---|---|---|
get | a getter function, used for indexing into a type | ✅ | ✅ |
set | a setter function, used for setting a value on a type | ✅ | ✅ |
sub | a subtraction function, used for subtracting two values | ✅ | ❌ |
add | an addition function, used for adding two values | ✅ | ❌ |
mul | a multiplication function, used for multiplying two values | ✅ | ❌ |
div | a division function, used for dividing two values | ✅ | ❌ |
rem | a remainder function, used for getting the remainder of two values | ✅ | ❌ |
neg | a negation function, used for negating a value | ✅ | ❌ |
pow | a power function, used for raising a value to a power | ✅ | ❌ |
eq | an equality function, used for checking if two values are equal | ✅ | ❌ |
lt | a less than function, used for checking if a value is less than another | ✅ | ❌ |
iter | an iterator function, used for iterating over a value | ❌ | ✅ |
display_ref | a display function, used for displaying a reference to a value | ❌ | ✅ |
display_value | a display function, used for displaying a mutable reference to a value | ❌ | ✅ |
In this context overridable
indicates whether language implementations will look for a specific function on your type before looking at the generic ReflectReference
namespace. You can still remove the existing registration for these functions on the ReflectReference
namespace if you want to replace them with your own implementation.
Modifying Script Contexts
You should be able to achieve what you need by registering script functions in most cases. However sometimes you might want to override the way contexts are loaded, or how the runtime is initialized.
This is possible using Context Initializers
and Context Pre Handling Initializers
as well as Runtime Initializers
.
It is however always reccomened to use the dynamic script function registry whenever possible, as it is more flexible and easier to use. It also allows you to introspect available functions easier.
Context Initializers
For example, let's say you want to set a dynamic amount of globals in your script, depending on some setting in your app.
You could do this by customizing the scripting plugin:
let plugin = LuaScriptingPlugin::default().add_context_initializer(|script_id: &str, context: &mut Lua| {
let globals = context.globals();
for i in 0..10 {
globals.set(i, i);
}
Ok(())
});
app.add_plugins(plugin)
The above will run every time the script is loaded or re-loaded and before it handles any callbacks.
Context Pre Handling Initializers
If you want to customize your context before every time it's about to handle events (and when it's loaded + reloaded), you can use Context Pre Handling Initializers
:
let plugin = LuaScriptingPlugin::default().add_context_pre_handling_initializer(|script_id: &str, entity: Entity, context: &mut Lua| {
let globals = context.globals();
globals.set("script_name", script_id.to_owned());
Ok(())
});
Runtime Initializers
Some scripting languages, have the concept of a runtime
. This is a global object which is shared between all contexts. You can customize this object using Runtime Initializers
:
let plugin = SomeScriptingPlugin::default().add_runtime_initializer(|runtime: &mut Runtime| {
runtime.set_max_stack_size(1000);
Ok(())
});
In the case of Lua, the runtime type is ()
i.e. This is because mlua
does not have a separate runtime concept.
Accessing the World in Initializers
You can access the world in these initializers by using the thread local: ThreadWorldContainer
:
let plugin = LuaScriptingPlugin::default();
plugin.add_context_initializer(|script_id: &str, context: &mut Lua| {
let world = ThreadWorldContainer.try_get_world().unwrap();
world.with_resource::<MyResource>(|res| println!("My resource: {:?}", res));
Ok(())
});
Shared Contexts
By default BMS will create an individual script context, or sandbox, for each script that is run. This means that each script will have its own set of global variables and functions that are isolated from other scripts. However, sometimes this might not be desirable, if you aren't worried about scripts interfering with each other, or if you want to easilly share data between scripts. In these cases, you can use shared contexts.
Enabling Shared Contexts
You can enable shared contexts by configuring the relevant scripting plugin like so:
let mut plugin = LuaScriptingPlugin::default().enable_context_sharing();
app.add_plugins(plugin);
Context Loading Settings
All context loading settings are stored in a separate resource per scripting plugin namely: ContextLoadingSettings<Plugin>
.
The settings are as follows:
loader
- the load and unload strategy for contexts. Each scripting plugin will have a load and unload function which is hooked up through hereassigner
- the strategy for assigning/unassigning contexts to scripts. This is used to determine how to assign a context to a script when it is run, and what to do with the context when the script is finished.context_initializers
- stores all context initializers for the plugincontext_pre_handling_initializers
- stores all context pre-handling initializers for the plugin
More advanced applications might want to customize these settings to suit their needs.
Script ID mapping
Every script is currently identified by a unique ID.
ID's are derived from the script asset path for scripts loaded via the asset system.
By default this is an identity mapping, but you can override this by modifying the AssetPathToScriptIdMapper
inside the ScriptAssetSettings
resource before loading the script.
Scripting Reference
This part of the book covers the user-facing API of the scripting languages supported by BMS. This will be where you will want to forward your script users to get started with scripting in BMS.
If you are a modder, welcome! 👋, apologies for the rust-centricity of this guide, we are working on it!
Globals
Scripts will have access to a few global variables in most callbacks:
world
: a static reference to the world, with all sorts of functions availableentity
: the entity the script is attached to, not available on load/unload callbacksscript_id
: the ID of the current script
Core Bindings
The core bindings are manually written utilities for interacting with the Bevy
world and everything contained within it. These bindings are used to create and manipulate entities, components, resources, and systems.
Every language BMS supports will support these.
World
The World
is the entry point for interacting with Bevy
. It is provided to scripts under either the world
or World
static variable.
get_type_by_name
Returns either a ScriptComponentRegistration
or ScriptResourceRegistration
depending on the type of the type requested. If the type is neither returns a ScriptTypeRegistration
.
Arguments:
Argument | Type | Description |
---|---|---|
type_name | String | The name of the type to get, this can be either the short type name, i.e. my_type or the long name i.e. my_crate::my_module::my_type |
Returns:
Return | Description |
---|---|
Option<ScriptTypeRegistration OR scriptComponentRegistration OR scriptResourceRegistration> | The registration for the type if it exists, otherwise None |
MyType = world.get_type_by_name("MyType")
if MyType == nil then
print("MyType not found")
end
get_component
Arguments:
Argument | Type | Description |
---|---|---|
entity | Entity | The entity to get the component from |
registration | ScriptTypeRegistration | The type registration as returned by get_type_by_name of the component |
Returns:
Return | Description |
---|---|
Option<ReflectReference> | The reference to the component if it exists, otherwise None |
local component = world.get_component(entity, MyType)
if component ~= nil then
print("found component:" .. component)
end
has_component
Arguments:
Argument | Type | Description |
---|---|---|
entity | Entity | The entity to check the component for |
registration | ScriptTypeRegistration | The type registration as returned by get_type_by_name of the component |
Returns:
Return | Description |
---|---|
bool | true if the entity has the component, otherwise false |
if world.has_component(entity, MyType) then
print("Entity has MyType")
end
remove_component
Arguments:
Argument | Type | Description |
---|---|---|
entity | Entity | The entity to remove the component from |
registration | ScriptTypeRegistration | The type registration as returned by get_type_by_name of the component |
world.remove_component(entity, MyType)
get_resource
Arguments:
Argument | Type | Description |
---|---|---|
registration | ScriptTypeRegistration | The type registration as returned by get_type_by_name of the resource |
Returns:
Return | Description |
---|---|
Option<ReflectReference> | The resource if it exists, otherwise None |
local resource = world.get_resource(MyType)
if resource ~= nil then
print("found resource:" .. resource)
end
has_resource
Arguments:
Argument | Type | Description |
---|---|---|
registration | ScriptTypeRegistration | The type registration as returned by get_type_by_name of the resource |
Returns:
Return | Description |
---|---|
bool | true if the resource exists, otherwise false |
local hasResource = world.has_resource(MyType)
remove_resource
Arguments:
Argument | Type | Description |
---|---|---|
registration | ScriptTypeRegistration | The type registration as returned by get_type_by_name of the resource |
world.remove_resource(MyType)
add_default_component
Arguments:
Argument | Type | Description |
---|---|---|
entity | Entity | The entity to add the component to |
registration | ScriptTypeRegistration | The type registration as returned by get_type_by_name of the component |
world.add_default_component(entity, MyType)
insert_component
Inserts or applies the given value to the component of the entity. If the component does not exist it will be added.
Arguments:
Argument | Type | Description |
---|---|---|
entity | Entity | The entity to add the component to |
registration | ScriptTypeRegistration | The type registration as returned by get_type_by_name of the component |
component | ReflectReference | A reference to an existing component value to be inserted |
local existingComponent = world.get_component(otherEntity, MyType)
world.insert_component(entity, MyType, existingComponent)
spawn
Returns:
Return | Description |
---|---|
Entity | The spawned entity |
local entity = world.spawn()
insert_children
Arguments:
Argument | Type | Description |
---|---|---|
entity | Entity | The parent entity |
index | usize | The index to insert the children at |
children | Vec<Entity> | The children entities to insert |
world.insert_children(parent, 1, {child1, child2})
push_children
Arguments:
Argument | Type | Description |
---|---|---|
entity | Entity | The parent entity |
children | Vec<Entity> | The children entities to push |
world.push_children(parent, {child1, child2})
get_children
Arguments:
Argument | Type | Description |
---|---|---|
entity | Entity | The parent entity |
Returns:
Return | Description |
---|---|
Vec<Entity> | The children entities |
local children = world.get_children(parent)
for _, child in pairs(children) do
print("child: " .. child)
end
get_parent
Arguments:
Argument | Type | Description |
---|---|---|
entity | Entity | The child entity |
Returns:
Return | Description |
---|---|
Option<Entity> | The parent entity if it exists, otherwise None |
local parent = world.get_parent(child)
if parent ~= nil then
print("parent: " .. parent)
end
despawn
Arguments:
Argument | Type | Description |
---|---|---|
entity | Entity | The entity to despawn |
world.despawn(entity)
despawn_descendants
Arguments:
Argument | Type | Description |
---|---|---|
entity | Entity | The entity to despawn descendants of |
world.despawn_descendants(entity)
despawn_recursive
Arguments:
Argument | Type | Description |
---|---|---|
entity | Entity | The entity to despawn recursively |
world.despawn_recursive(entity)
has_entity
Arguments:
Argument | Type | Description |
---|---|---|
entity | Entity | The entity to check |
Returns:
Return | Description |
---|---|
bool | true if the entity exists, otherwise false |
local exists = world.has_entity(entity)
if exists then
print("entity exists")
end
query
Returns:
Return | Description |
---|---|
ScriptQueryBuilder | The query builder |
local queryBuilder = world.query()
exit
Send the exit signal to the application, will gracefully shutdown the application.
world.exit()
ReflectReference
ReflectReferences are simply references to data living either in:
- A component
- A resource
- The allocator
Reflect references contain a standard interface which operates over the reflection layer exposed by Bevy
and also provides a way to call various dynamic functions registered on the underlying pointed to data.
display_ref
Arguments:
Argument | Type | Description |
---|---|---|
s | ReflectReference | The reference to display |
Returns:
Return | Description |
---|---|
String | The reference in string format |
print(ref:display_ref())
print(ref)
display_value
Arguments:
Argument | Type | Description |
---|---|---|
s | ReflectReference | The reference to display |
Returns:
Return | Description |
---|---|
String | The value in string format |
print(ref:display_value())
get
The index function, allows you to index into the reflect reference.
Arguments:
Argument | Type | Description |
---|---|---|
key | ScriptValue | The key to get the value for |
Returns:
Return | Description |
---|---|
ScriptValue | The value |
local value = ref:get(key)
-- same as
local value = ref.key
local value = ref[key]
local value = ref["key"]
-- for tuple structs
local valye = ref._1
set
Arguments:
Argument | Type | Description |
---|---|---|
key | ScriptValue | The key to set the value for |
value | ScriptValue | The value to set |
Returns:
Return | Description |
---|---|
ScriptValue | The result |
ref:set(key, value)
-- same as
ref.key = value
ref[key] = value
ref["key"] = value
-- for tuple structs
ref._1 = value
push
Generic push method, if the underlying type supports it, will push the value into the end of the reference.
Arguments:
Argument | Type | Description |
---|---|---|
value | ScriptValue | The value to push |
ref:push(value)
pop
Generic pop method, if the underlying type supports it, will pop the value from the end of the reference.
Arguments:
Argument | Type | Description |
---|---|---|
s | ReflectReference | The reference to pop from |
Returns:
Return | Description |
---|---|
ScriptValue | The popped value |
local value = ref:pop()
insert
Generic insert method, if the underlying type supports it, will insert the value at the key.
Arguments:
Argument | Type | Description |
---|---|---|
key | ScriptValue | The key to insert the value for |
value | ScriptValue | The value to insert |
ref:insert(key, value)
clear
Generic clear method, if the underlying type supports it, will clear the referenced container type.
Arguments:
Argument | Type | Description |
---|---|---|
s | ReflectReference | The reference to clear |
ref:clear()
len
Generic length method, if the underlying type supports it, will return the length of the referenced container or length relevant to the type itself (number of fields etc.).
Arguments:
Argument | Type | Description |
---|---|---|
s | ReflectReference | The reference to get the length of |
Returns:
Return | Description |
---|---|
usize | The length |
length = ref:len()
remove
Generic remove method, if the underlying type supports it, will remove the value at the key.
Arguments:
Argument | Type | Description |
---|---|---|
key | ScriptValue | The key to remove the value for |
Returns:
Return | Description |
---|---|
ScriptValue | The removed value |
local value = ref:remove(key)
iter
The iterator function, returns a function which can be called to iterate over the reference.
Arguments:
Argument | Type | Description |
---|---|---|
s | ReflectReference | The reference to iterate over |
Returns:
Return | Description |
---|---|
ScriptFunctionMut | The iterator function |
local iter = ref:iter()
local val = iter()
while val do
print(val)
next = iter()
end
-- same as
for val in pairs(ref) do
print(val)
end
functions
Returns a list of functions that can be called on the reference.
Returns:
Return | Description |
---|---|
Vec<FunctionInfo> | The list of functions |
local functions = ref:functions()
for _, func in ipairs(functions) do
print(func.name)
end
ScriptTypeRegistration
A reference to a type registration, in general think of this as a handle to a type.
type_name
Arguments:
Argument | Type | Description |
---|---|---|
s | ScriptTypeRegistration | The type registration as returned by get_type_by_name |
Returns:
Return | Description |
---|---|
String | The type name |
local name = MyType:type_name()
short_name
Arguments:
Argument | Type | Description |
---|---|---|
s | ScriptTypeRegistration | The type registration as returned by get_type_by_name |
Returns:
Return | Description |
---|---|
String | The short name |
local name = MyType:short_name()
is_resource
Arguments:
Argument | Type | Description |
---|---|---|
s | ScriptTypeRegistration | The type registration as returned by get_type_by_name |
Returns:
Return | Description |
---|---|
bool | true if the type is a resource, otherwise false |
if MyType:is_resource() then
print("MyType is a resource")
end
is_component
Arguments:
Argument | Type | Description |
---|---|---|
s | ScriptTypeRegistration | The type registration as returned by get_type_by_name |
Returns:
Return | Description |
---|---|
bool | true if the type is a component, otherwise false |
if MyType:is_component() then
print("MyType is a component")
end
ScriptComponentRegistration
A reference to a component type's registration, in general think of this as a handle to a type.
type_name
Arguments:
Argument | Type | Description |
---|---|---|
s | ScriptComponentRegistration | The type registration as returned by get_type_by_name |
Returns:
Return | Description |
---|---|
String | The type name |
local name = MyType:type_name()
short_name
Arguments:
Argument | Type | Description |
---|---|---|
s | ScriptComponentRegistration | The type registration as returned by get_type_by_name |
Returns:
Return | Description |
---|---|
String | The short name |
local name = MyType:short_name()
ScriptResourceRegistration
A reference to a resource type's registration, in general think of this as a handle to a type.
type_name
Arguments:
Argument | Type | Description |
---|---|---|
s | ScriptResourceRegistration | The type registration as returned by get_type_by_name |
Returns:
Return | Description |
---|---|
String | The type name |
local name = MyType:type_name()
short_name
Arguments:
Argument | Type | Description |
---|---|---|
s | ScriptResourceRegistration | The type registration as returned by get_type_by_name |
Returns:
Return | Description |
---|---|
String | The short name |
local name = MyType:short_name()
ScriptQueryBuilder
The query builder is used to build queries for entities with specific components. Can be used to interact with arbitrary entities in the world.
component
Adds a component to the query, this will be accessible in the query results under the index corresponding to the index of this component in the query.
Arguments:
Argument | Type | Description |
---|---|---|
s | ScriptQueryBuilder | The query builder |
component | ScriptComponentRegistration | The component to query for |
Returns:
Return | Description |
---|---|
ScriptQueryBuilder | The updated query builder |
query:component(MyType):component(MyOtherType)
with
Arguments:
Argument | Type | Description |
---|---|---|
s | ScriptQueryBuilder | The query builder |
with | ScriptComponentRegistration | The component to include in the query |
Returns:
Return | Description |
---|---|
ScriptQueryBuilder | The updated query builder |
query:with(MyType):with(MyOtherType)
without
Arguments:
Argument | Type | Description |
---|---|---|
s | ScriptQueryBuilder | The query builder |
without | ScriptComponentRegistration | The component to exclude from the query |
Returns:
Return | Description |
---|---|
ScriptQueryBuilder | The updated query builder |
query:without(MyType):without(MyOtherType)
build
Arguments:
Argument | Type | Description |
---|---|---|
s | ScriptQueryBuilder | The query builder |
Returns:
Return | Description |
---|---|
Vec<ScriptQueryResult> | The query results |
local results = query:build()
for _, result in pairs(results) do
print(result)
end
ScriptQueryResult
The result of a query, built by the query builder.
entity
Arguments:
Argument | Type | Description |
---|---|---|
s | ScriptQueryResult | The query result |
Returns:
Return | Description |
---|---|
Entity | The entity |
local entity = result:entity()
components
Arguments:
Argument | Type | Description |
---|---|---|
s | ScriptQueryResult | The query result |
Returns:
Return | Description |
---|---|
Vec<ReflectReference> | The components |
for _, component in pairs(result:components()) do
print(component)
end
Core Callbacks
On top of callbacks which are registered by your application, BMS provides a set of core callbacks which are always available.
The two core callbacks are:
on_script_loaded
on_script_unloaded
on_script_loaded
This will be called right after a script has been loaded or reloaded. This is a good place to initialize your script. You should avoid placing a lot of logic into the global body of your script, and instead put it into this callback. Otherwise errors in the initialization will fail the loading of the script.
This callback will not have access to the entity
variable, as when the script is being loaded it's not attached to an entity yet.
print("you can also use this space, but it's not recommended")
function on_script_loaded()
print("Hello world")
end
on_script_unloaded
This will be called right before a script is unloaded. This is a good place to clean up any resources that your script has allocated. Note this is not called when a script is reloaded, only when it is being removed from the system.
This callback will not have access to the entity
variable, as when the script is being unloaded it might not be attached to an entity.
function on_script_unloaded()
print("Goodbye world")
end
Developing BMS
This section is for developers who want to contribute to the BMS project. It covers various topics necessary for understanding the project and contributing to it.
Contributing
Please see the code of conduct as well as the contributing guidelines first.
Setup
this crate contains a work in progress xtask
setup which in theory should allow you to setup everything you need for local development by running:
cargo xtask init
This command currently supports the following IDE's
If you'd like to add support for another IDE, please feel free to open a PR!
Adding New Languages to BMS
If you are interested in adding a new language to BMS, please read this section first. Adding a new language is a non-trivial task depending on the language you want to add. It also requires a lot of effort and time to maintain a new language, so unless the language you want to add is widely used and has a large community, it might not be worth the effort.
Evaluating Feasibility
In order for a language to work well with BMS it's necessary it supports the following features:
- Interoperability with Rust. If you can't call it from Rust easilly and there is no existing crate that can do it for you, it's a no-go.
- First class functions. Or at least the ability to call an arbitrary function with an arbitrary number of arguments from a script. Without this feature, you would need to separately generate code for the bevy bindings which is painful and goes against the grain of BMS.
First Classs Functions
They don't necessarily have to be first class from the script POV, but they need to be first class from the POV of the host language. This means that the host language needs to be able to call a function with an arbitrary number of arguments.
Examples
Let's say your language supports a Value
type which can be returned to the script. And it has a Value::Function
variant. The type on the Rust side would look something like this:
pub enum Value {
Function(Arc<Fn(&[Value]) -> Value>),
// other variants
}
This is fine, and can be integrated with BMS. Since an Fn function can be a closure capturing a DynamicScriptFunction
. If there is no support for FnMut
closures though, you might face issues in the implementation. Iterators in bevy_mod_scripting_functions
for example use DynamicScriptFunctionMut
which cannot work with Fn
closures.
Now let's imagine instead another language with a similar enum, supports this type instead:
#![allow(unused)] fn main() { pub enum Value { Function(Arc<dyn Function>), // other variants } pub trait Function { fn call(&self, args: Vec<Value>) -> Value; fn num_params() -> usize; } }
This implies that to call this function, you need to be able to know the amount of arguments it expects at COMPILE time. This is not compatibile with dynamic functions, and would require a lot of code generation to make work with BMS. Languages with no support for dynamic functions are not compatible with BMS.
Interoperability with Rust
Not all languages can easilly be called from Rust. Lua has a wonderful crate which works out the ffi and safety issues for us. But not all languages have this luxury. If you can't call it from Rust easilly and there is no existing crate that can do it for you, integrating with BMS might not be the best idea.
Necessary Features
In order for a language to be called "implemented" in BMS, it needs to support the following features:
- Every script function which is registered on a type's namespace must:
- Be callable on a
ReflectReference
representing object of that type in the script
local my_reference = ... my_reference:my_Registered_function()
- If it's static it must be callable from a global proxy object for that type, i.e.
MyType.my_static_function()
- Be callable on a
ReflectReferences
must support a set of basic features:- Access to fields via reflection i.e.:
local my_reference = ... my_reference.my_field = 5 print(my_reference.my_field)
- Basic operators and standard operations are overloaded with the appropriate standard dynamic function registered:
- Addition: dispatches to the
add
binary function on the type - Multiplication: dispatches to the
mul
binary function on the type - Division: dispatches to the
div
binary function on the type - Subtraction: dispatches to the
sub
binary function on the type - Modulo: dispatches to the
rem
binary function on the type - Negation: dispatches to the
neg
unary function on the type - Exponentiation: dispatches to the
pow
binary function on the type - Equality: dispatches to the
eq
binary function on the type - Less than: dispatches to the
lt
binary function on the type - Length: calls the
len
method onReflectReference
or on the table if the value is one. - Iteration: dispatches to the
iter
method onReflectReference
which returns an iterator function, this can be repeatedly called until it returnsScriptValue::Unit
to signal the end of the iteration. - Print: calls the
display_ref
method onReflectReference
or on the table if the value is one.
- Addition: dispatches to the
- Script handlers, loaders etc. must be implemented such that the
ThreadWorldContainer
is set for every interaction with script contexts, or anywhere else it might be needed.