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:

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 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 the ScriptValue::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 NameDescriptionOverridable?Has Default Implementation?
geta getter function, used for indexing into a type
seta setter function, used for setting a value on a type
suba subtraction function, used for subtracting two values
addan addition function, used for adding two values
mula multiplication function, used for multiplying two values
diva division function, used for dividing two values
rema remainder function, used for getting the remainder of two values
nega negation function, used for negating a value
powa power function, used for raising a value to a power
eqan equality function, used for checking if two values are equal
lta less than function, used for checking if a value is less than another
iteran iterator function, used for iterating over a value
display_refa display function, used for displaying a reference to a value
display_valuea 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 here
  • assigner - 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 plugin
  • context_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 available
  • entity: the entity the script is attached to, not available on load/unload callbacks
  • script_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:

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

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)

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:

ArgumentTypeDescription
entityEntityThe entity to add the component to
registrationScriptTypeRegistrationThe type registration as returned by get_type_by_name of the component
componentReflectReferenceA reference to an existing component value to be inserted
local existingComponent = world.get_component(otherEntity, MyType)
world.insert_component(entity, MyType, existingComponent)

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

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

functions

Returns a list of functions that can be called on the reference.

Returns:

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

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

ScriptComponentRegistration

A reference to a component type's registration, in general think of this as a handle to a type.

type_name

Arguments:

ArgumentTypeDescription
sScriptComponentRegistrationThe type registration as returned by get_type_by_name

Returns:

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

short_name

Arguments:

ArgumentTypeDescription
sScriptComponentRegistrationThe type registration as returned by get_type_by_name

Returns:

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

ArgumentTypeDescription
sScriptResourceRegistrationThe type registration as returned by get_type_by_name

Returns:

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

short_name

Arguments:

ArgumentTypeDescription
sScriptResourceRegistrationThe type registration as returned by get_type_by_name

Returns:

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

ArgumentTypeDescription
sScriptQueryBuilderThe query builder
componentScriptComponentRegistrationThe component to query for

Returns:

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

with

Arguments:

ArgumentTypeDescription
sScriptQueryBuilderThe query builder
withScriptComponentRegistrationThe component to include in the query

Returns:

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

without

Arguments:

ArgumentTypeDescription
sScriptQueryBuilderThe query builder
withoutScriptComponentRegistrationThe 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.

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()
    
  • 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 on ReflectReference or on the table if the value is one.
      • Iteration: dispatches to the iter method on ReflectReference which returns an iterator function, this can be repeatedly called until it returns ScriptValue::Unit to signal the end of the iteration.
      • Print: calls the display_ref method on ReflectReference or on the table if the value is one.
  • 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.