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());
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 are processed via commands, so any asset events will be processed at the next command execution point running after BMS internal asset systems.
Attaching Scripts
Once you have scripts discovered and loaded, you'll want to run them. At the moment BMS supports one method of triggering scripts, and that is by attaching them to entities via ScriptComponent
's and then sending script event's which trigger callbacks on the scripts.
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"]));
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()
print("hello from event time")
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:
// 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>) {
writer.send(ScriptCallbackEvent::new_for_all(
OnEvent,
vec![ScriptValue::Unit],
));
}
Note the second argument is the payload we are sending with the event, in this case we are sending an empty payload.
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.
Controlling Script Bindings
In this book we reffer 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.
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
GetInnerTypeDependencies
- Each return type must also implement
GetInnerTypeDependencies
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 GetInnerTypeDependencies
, 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)
},
);
This will allow you to call this function within lua like so:
hello_world("hi from lua!")
Context Arguments
Each script function call always receives 2 context arguments, namely:
CallerContext
WorldCallbackAccess
The first one is configured by the caller, and 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.
The second argument gives you access to the world from your function.
You can opt-in to receive these arguments by adding them to your closure arguments in the above order (either both or just one)
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.
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!
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
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> | 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)
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 date living either:
- In a component
- In a resource
- In 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
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
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 | ScriptTypeRegistration | 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 | ScriptTypeRegistration | 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 | ScriptTypeRegistration | 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.
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.
function on_script_unloaded()
print("Goodbye world")
end