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:

LanguageFeature Flag
Lua51lua51
Lua52lua54
Lua53lua53
Lua54lua54
Luajitluajit
Luajit52luajit52
Luauluau
Rhairhai
Runerune

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.

FeatureDescription
core_functionsIf enabled, will enable all core functions, i.e. bevy integrations which let you interact with Bevy via reflection
bevy_bindingsIf 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_asyncEnables mlua/async
mlua_serializeEnables mlua/serialize
mlua_macrosEnables mlua/macros
unsafe_lua_modulesAllows 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 the ScriptValue::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:

ArgumentTypeDescription
type_nameStringThe 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:

ReturnDescription
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:

ArgumentTypeDescription
entityEntityThe entity to get the component from
registrationScriptTypeRegistrationThe type registration as returned by get_type_by_name of the component

Returns:

ReturnDescription
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:

ArgumentTypeDescription
entityEntityThe entity to check the component for
registrationScriptTypeRegistrationThe type registration as returned by get_type_by_name of the component

Returns:

ReturnDescription
booltrue if the entity has the component, otherwise false
if world.has_component(entity, MyType) then
    print("Entity has MyType")
end

remove_component

Arguments:

ArgumentTypeDescription
entityEntityThe entity to remove the component from
registrationScriptTypeRegistrationThe type registration as returned by get_type_by_name of the component
world.remove_component(entity, MyType)

get_resource

Arguments:

ArgumentTypeDescription
registrationScriptTypeRegistrationThe type registration as returned by get_type_by_name of the resource

Returns:

ReturnDescription
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:

ArgumentTypeDescription
registrationScriptTypeRegistrationThe type registration as returned by get_type_by_name of the resource

Returns:

ReturnDescription
booltrue if the resource exists, otherwise false
local hasResource = world.has_resource(MyType)

remove_resource

Arguments:

ArgumentTypeDescription
registrationScriptTypeRegistrationThe type registration as returned by get_type_by_name of the resource
world.remove_resource(MyType)

add_default_component

Arguments:

ArgumentTypeDescription
entityEntityThe entity to add the component to
registrationScriptTypeRegistrationThe type registration as returned by get_type_by_name of the component
world.add_default_component(entity, MyType)

spawn

Returns:

ReturnDescription
EntityThe spawned entity
local entity = world.spawn()

insert_children

Arguments:

ArgumentTypeDescription
entityEntityThe parent entity
indexusizeThe index to insert the children at
childrenVec<Entity>The children entities to insert
world.insert_children(parent, 1, {child1, child2})

push_children

Arguments:

ArgumentTypeDescription
entityEntityThe parent entity
childrenVec<Entity>The children entities to push
world.push_children(parent, {child1, child2})

get_children

Arguments:

ArgumentTypeDescription
entityEntityThe parent entity

Returns:

ReturnDescription
Vec<Entity>The children entities
local children = world.get_children(parent)
for _, child in pairs(children) do
    print("child: " .. child)
end

get_parent

Arguments:

ArgumentTypeDescription
entityEntityThe child entity

Returns:

ReturnDescription
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:

ArgumentTypeDescription
entityEntityThe entity to despawn
world.despawn(entity)

despawn_descendants

Arguments:

ArgumentTypeDescription
entityEntityThe entity to despawn descendants of
world.despawn_descendants(entity)

despawn_recursive

Arguments:

ArgumentTypeDescription
entityEntityThe entity to despawn recursively
world.despawn_recursive(entity)

has_entity

Arguments:

ArgumentTypeDescription
entityEntityThe entity to check

Returns:

ReturnDescription
booltrue if the entity exists, otherwise false
local exists = world.has_entity(entity)
if exists then
    print("entity exists")
end

query

Returns:

ReturnDescription
ScriptQueryBuilderThe 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:

ArgumentTypeDescription
sReflectReferenceThe reference to display

Returns:

ReturnDescription
StringThe reference in string format
print(ref:display_ref())
print(ref)

display_value

Arguments:

ArgumentTypeDescription
sReflectReferenceThe reference to display

Returns:

ReturnDescription
StringThe value in string format
print(ref:display_value())

get

The index function, allows you to index into the reflect reference.

Arguments:

ArgumentTypeDescription
keyScriptValueThe key to get the value for

Returns:

ReturnDescription
ScriptValueThe 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:

ArgumentTypeDescription
keyScriptValueThe key to set the value for
valueScriptValueThe value to set

Returns:

ReturnDescription
ScriptValueThe 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:

ArgumentTypeDescription
valueScriptValueThe 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:

ArgumentTypeDescription
sReflectReferenceThe reference to pop from

Returns:

ReturnDescription
ScriptValueThe popped value
local value = ref:pop()

insert

Generic insert method, if the underlying type supports it, will insert the value at the key.

Arguments:

ArgumentTypeDescription
keyScriptValueThe key to insert the value for
valueScriptValueThe value to insert
ref:insert(key, value)

clear

Generic clear method, if the underlying type supports it, will clear the referenced container type.

Arguments:

ArgumentTypeDescription
sReflectReferenceThe 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:

ArgumentTypeDescription
sReflectReferenceThe reference to get the length of

Returns:

ReturnDescription
usizeThe length
length = ref:len()

remove

Generic remove method, if the underlying type supports it, will remove the value at the key.

Arguments:

ArgumentTypeDescription
keyScriptValueThe key to remove the value for

Returns:

ReturnDescription
ScriptValueThe removed value
local value = ref:remove(key)

iter

The iterator function, returns a function which can be called to iterate over the reference.

Arguments:

ArgumentTypeDescription
sReflectReferenceThe reference to iterate over

Returns:

ReturnDescription
ScriptFunctionMutThe 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:

ArgumentTypeDescription
sScriptTypeRegistrationThe type registration as returned by get_type_by_name

Returns:

ReturnDescription
StringThe type name
local name = MyType:type_name()

short_name

Arguments:

ArgumentTypeDescription
sScriptTypeRegistrationThe type registration as returned by get_type_by_name

Returns:

ReturnDescription
StringThe short name
local name = MyType:short_name()

is_resource

Arguments:

ArgumentTypeDescription
sScriptTypeRegistrationThe type registration as returned by get_type_by_name

Returns:

ReturnDescription
booltrue if the type is a resource, otherwise false
if MyType:is_resource() then
    print("MyType is a resource")
end

is_component

Arguments:

ArgumentTypeDescription
sScriptTypeRegistrationThe type registration as returned by get_type_by_name

Returns:

ReturnDescription
booltrue 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:

ArgumentTypeDescription
sScriptQueryBuilderThe query builder
componentScriptTypeRegistrationThe component to query for

Returns:

ReturnDescription
ScriptQueryBuilderThe updated query builder
query:component(MyType):component(MyOtherType)

with

Arguments:

ArgumentTypeDescription
sScriptQueryBuilderThe query builder
withScriptTypeRegistrationThe component to include in the query

Returns:

ReturnDescription
ScriptQueryBuilderThe updated query builder
query:with(MyType):with(MyOtherType)

without

Arguments:

ArgumentTypeDescription
sScriptQueryBuilderThe query builder
withoutScriptTypeRegistrationThe component to exclude from the query

Returns:

ReturnDescription
ScriptQueryBuilderThe updated query builder
query:without(MyType):without(MyOtherType)

build

Arguments:

ArgumentTypeDescription
sScriptQueryBuilderThe query builder

Returns:

ReturnDescription
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:

ArgumentTypeDescription
sScriptQueryResultThe query result

Returns:

ReturnDescription
EntityThe entity
local entity = result:entity()

components

Arguments:

ArgumentTypeDescription
sScriptQueryResultThe query result

Returns:

ReturnDescription
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