Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

bevy_scriptum 📜

bevy_scriptum is a a plugin for Bevy that allows you to write some of your game or application logic in a scripting language.

Supported scripting languages/runtimes

language/runtimecargo featuredocumentation chapter
🌙 LuaJITlualink
🌾 Rhairhailink
💎 Rubyrubylink

Documentation book is available here 📖

Full API docs are available at docs.rs 🧑‍💻

bevy_scriptum's main advantages include:

  • low-boilerplate
  • easy to use
  • asynchronicity with a promise-based API
  • flexibility
  • hot-reloading

Scripts are separate files that can be hot-reloaded at runtime. This allows you to quickly iterate on your game logic without having to recompile it.

All you need to do is register callbacks on your Bevy app like this:

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|runtime| {
             runtime.add_function(String::from("hello_bevy"), || {
               println!("hello bevy, called from script");
             });
        })
        .run();
}

And you can call them in your scripts like this:

hello_bevy()

Every callback function that you expose to the scripting language is also a Bevy system, so you can easily query and mutate ECS components and resources just like you would in a regular Bevy system:

extern crate bevy;
extern crate bevy_ecs;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

#[derive(Component)]
struct Player;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|runtime| {
            runtime.add_function(
                String::from("print_player_names"),
                |players: Query<&Name, With<Player>>| {
                    for player in &players {
                        println!("player name: {}", player);
                    }
                },
            );
        })
        .run();
}

You can also pass arguments to your callback functions, just like you would in a regular Bevy system - using In structs with tuples:

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|runtime| {
            runtime.add_function(
                String::from("fun_with_string_param"),
                |In((x,)): In<(String,)>| {
                    println!("called with string: '{}'", x);
                },
            );
        })
        .run();
}

which you can then call in your script like this:

fun_with_string_param("Hello world!")

Usage

Add the following to your Cargo.toml:

[dependencies]
bevy_scriptum = { version = "0.9", features = ["lua"] }

or execute cargo add bevy_scriptum --features lua from your project directory.

You can now start exposing functions to the scripting language. For example, you can expose a function that prints a message to the console:

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|runtime| {
           runtime.add_function(
               String::from("my_print"),
               |In((x,)): In<(String,)>| {
                   println!("my_print: '{}'", x);
               },
           );
        })
        .run();
}

Then you can create a script file in assets directory called script.lua that calls this function:

my_print("Hello world!")

And spawn an entity with attached Script component with a handle to a script source file:

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|runtime| {
           runtime.add_function(
               String::from("my_print"),
               |In((x,)): In<(String,)>| {
                   println!("my_print: '{}'", x);
               },
           );
        })
        .add_systems(Startup,|mut commands: Commands, asset_server: Res<AssetServer>| {
            commands.spawn(Script::<LuaScript>::new(asset_server.load("script.lua")));
        })
        .run();
}

You should then see my_print: 'Hello world!' printed in your console.

Provided examples

You can also try running provided examples by cloning this repository and running cargo run --example <example_name>_<language_name>. For example:

cargo run --example hello_world_lua

The examples live in examples directory and their corresponding scripts live in assets/examples directory within the repository.

Promises - getting return values from scripts

Every function called from script returns a promise that you can call :and_then with a callback function on. This callback function will be called when the promise is resolved, and will be passed the return value of the function called from script. For example:

get_player_name():and_then(function(name)
    print(name)
end)

which will print out John when used with following exposed function:

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn main() {
    App::new()
       .add_plugins(DefaultPlugins)
       .add_scripting::<LuaRuntime>(|runtime| {
               runtime.add_function(String::from("get_player_name"), || String::from("John"));
       });
}

Access entity from script

A variable called entity is automatically available to all scripts - it represents bevy entity that the Script component is attached to. It exposes index property that returns bevy entity index. It is useful for accessing entity's components from scripts. It can be used in the following way:

print("Current entity index: " .. entity.index)

entity variable is currently not available within promise callbacks.

Contributing

Contributions are welcome! Feel free to open an issue or submit a pull request.

License

bevy_scriptum is licensed under either of the following, at your option: Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) or MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)

Runtimes

This chapter demonstrates how to work with bevy_scriptum when using a specific runtime.

Lua

This chapter demonstrates how to work with bevy_scriptum when using Lua language runtime.

Installation

Add the following to your Cargo.toml:

[dependencies]
bevy = "0.16"
bevy_scriptum = { version = "0.9", features = ["lua"] }

If you need a different version of bevy you need to use a matching bevy_scriptum version according to the bevy support matrix

Hello World

After you are done installing the required crates, you can start developing your first game or application using bevy_scriptum.

To start using the library you need to first import some structs and traits with Rust use statements.

For convenience there is a main "prelude" module provided called bevy_scriptum::prelude and a prelude for each runtime you have enabled as a create feature.

You can now start exposing functions to the scripting language. For example, you can expose a function that prints a message to the console:

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|runtime| {
           runtime.add_function(
               String::from("my_print"),
               |In((x,)): In<(String,)>| {
                   println!("my_print: '{}'", x);
               },
           );
        })
        .run();
}

Then you can create a script file in assets directory called script.lua that calls this function:

my_print("Hello world!")

And spawn an entity with attached Script component with a handle to a script source file:

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|runtime| {
           runtime.add_function(
               String::from("my_print"),
               |In((x,)): In<(String,)>| {
                   println!("my_print: '{}'", x);
               },
           );
        })
        .add_systems(Startup,|mut commands: Commands, asset_server: Res<AssetServer>| {
            commands.spawn(Script::<LuaScript>::new(asset_server.load("script.lua")));
        })
        .run();
}

You should then see my_print: 'Hello world!' printed in your console.

Spawning scripts

To spawn a Lua script you will need to get a handle to a script asset using bevy's AssetServer.

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn my_spawner(mut commands: Commands, assets_server: Res<AssetServer>) {
    commands.spawn(Script::<LuaScript>::new(
        assets_server.load("my_script.lua"),
    ));
}
}

After they scripts have been evaled by bevy_scriptum, the entities that they've been attached to will get the Script::<LuaScript> component stripped and instead LuaScriptData component will be attached.

So to query scipted entities you could do something like:

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn my_system(
    mut scripted_entities: Query<(Entity, &mut LuaScriptData)>,
) {
    for (entity, mut script_data) in &mut scripted_entities {
        // do something with scripted entities
    }
}
}

Calling Rust from Lua

To call a rust function from Lua first you need to register a function within Rust using builder pattern.

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|runtime| {
            // `runtime` is a builder that you can use to register functions
        })
        .run();
}

For example to register a function called my_rust_func you can do the following:

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|runtime| {
             runtime.add_function(String::from("my_rust_func"), || {
               println!("my_rust_func has been called");
             });
        })
        .run();
}

After you do that the function will be available to Lua code in your spawned scripts.

my_rust_func()

Registered functions can also take parameters. A parameter can be any type that implements FromLua.

Since a registered callback function is a Bevy system, the parameters are passed to it as In struct with tuple, which has to be the first parameter of the closure.

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|runtime| {
             runtime.add_function(String::from("func_with_params"), |args: In<(String, i64)>| {
               println!("my_rust_func has been called with string {} and i64 {}", args.0.0, args.0.1);
             });
        })
        .run();
}

To make it look nicer you can destructure the In struct.

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|runtime| {
             runtime.add_function(String::from("func_with_params"), |In((a, b)): In<(String, i64)>| {
               println!("my_rust_func has been called with string {} and i64 {}", a, b);
             });
        })
        .run();
}

The above function can be called from Lua

func_with_params("abc", 123)

Return value via promise

Any registered rust function that returns a value will retrurn a promise when called within a script. By calling :and_then on the promise you can register a callback that will receive the value returned from Rust function.

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|runtime| {
             runtime.add_function(String::from("returns_value"), || {
                123
             });
        })
        .run();
}
returns_value():and_then(function (value)
    print(value) -- 123
end)

Calling Lua from Rust

To call a function defined in Lua

function on_update()
end

We need to acquire LuaRuntime resource within a bevy system. Then we will be able to call call_fn on it, providing the name of the function to call, LuaScriptData that has been automatically attached to entity after an entity with script attached has been spawned and its script evaluated, the entity and optionally some arguments.

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn call_lua_on_update_from_rust(
    mut scripted_entities: Query<(Entity, &mut LuaScriptData)>,
    scripting_runtime: ResMut<LuaRuntime>,
) {
    for (entity, mut script_data) in &mut scripted_entities {
        // calling function named `on_update` defined in lua script
        scripting_runtime
            .call_fn("on_update", &mut script_data, entity, ())
            .unwrap();
    }
}
}

We can also pass some arguments by providing a tuple or Vec as the last call_fn argument.

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn call_lua_on_update_from_rust(
    mut scripted_entities: Query<(Entity, &mut LuaScriptData)>,
    scripting_runtime: ResMut<LuaRuntime>,
) {
    for (entity, mut script_data) in &mut scripted_entities {
        scripting_runtime
            .call_fn("on_update", &mut script_data, entity, (123, String::from("hello")))
            .unwrap();
    }
}
}

They will be passed to on_update Lua function

function on_update(a, b)
    print(a) -- 123
    print(b) -- hello
end

Any type that implements IntoLua can be passed as an argument withing the tuple in call_fn.

Interacting with bevy in callbacks

Every registered function is also just a regular Bevy system.

That allows you to do anything you would do in a Bevy system.

You could for example create a callback system function that prints names of all entities with Player component.

extern crate bevy;
extern crate bevy_ecs;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

#[derive(Component)]
struct Player;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|runtime| {
            runtime.add_function(
                String::from("print_player_names"),
                |players: Query<&Name, With<Player>>| {
                    for player in &players {
                        println!("player name: {}", player);
                    }
                },
            );
        })
        .run();
}

In script:

print_player_names()

You can use functions that interact with Bevy entities and resources and take arguments at the same time. It could be used for example to mutate a component.

extern crate bevy;
extern crate bevy_ecs;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

#[derive(Component)]
struct Player {
    health: i32
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|runtime| {
            runtime.add_function(
                String::from("hurt_player"),
                |In((hit_value,)): In<(i32,)>, mut players: Query<&mut Player>| {
                    let mut player = players.single_mut().unwrap();
                    player.health -= hit_value;
                },
            );
        })
        .run();
}

And it could be called in script like:

hurt_player(5)

Builtin types

bevy_scriptum provides following types that can be used in Lua:

  • Vec3
  • BevyEntity

Vec3

Constructor

Vec3(x: number, y: number, z: number)

Properties

  • x: number
  • y: number
  • z: number

Example Lua usage

my_vec = Vec3(1, 2, 3)
set_translation(entity, my_vec)

Example Rust usage

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|runtime| {
             runtime.add_function(String::from("set_translation"), set_translation);
        })
        .run();
}

fn set_translation(
    In((entity, translation)): In<(BevyEntity, BevyVec3)>,
    mut entities: Query<&mut Transform>,
) {
    let mut transform = entities.get_mut(entity.0).unwrap();
    transform.translation = translation.0;
}

BevyEntity

Constructor

None - instances can only be acquired by using built-in entity global variable.

Properties

  • index: integer

Example Lua usage

print(entity.index)
pass_to_rust(entity)

Example Rust usage

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|runtime| {
             runtime.add_function(String::from("pass_to_rust"), |In((entity,)): In<(BevyEntity,)>| {
               println!("pass_to_rust called with entity: {:?}", entity);
             });
        })
        .run();
}

Builtin variables

entity

A variable called entity is automatically available to all scripts - it represents bevy entity that the Script component is attached to. It exposes index property that returns bevy entity index. It is useful for accessing entity's components from scripts. It can be used in the following way:

print("Current entity index: " .. entity.index)

entity variable is currently not available within promise callbacks.

Ruby

This chapter demonstrates how to work with bevy_scriptum when using Ruby language runtime. Ruby is currently only supported on Linux.

Installation

Ruby is currently only supported on Linux.

Ruby

To build bevy_scriptum with Ruby support a Ruby installation is needed to be present on your development machine.

The easiest way to produce a compatible Ruby installation is to use rbenv.

After installing rbenv along with its ruby-build plugin you can build and install a Ruby installation that will work with bevy_scriptum by executing:

CC=clang rbenv install 3.4.4

Above assumes that you also have clang installed on your system. For clang installation instruction consult your OS vendor provided documentation or clang official webiste.

If you rather not use rbenv you are free to supply your own installation of Ruby provided the following is true about it:

  • it is compiled with clang
  • it is compiled as a static library
  • it is accessible as ruby within PATH or RUBY environment variable is set to path of desired ruby binary.

Main Library

Add the following to your Cargo.toml:

[dependencies]
bevy = "0.16"
bevy_scriptum = { version = "0.9", features = ["ruby"] }

If you need a different version of bevy you need to use a matching bevy_scriptum version according to the bevy support matrix

Ruby also needs dynamic symbol resolution and since bevy_scriptum links Ruby statically the following build.rs file is needed to be present in project root directory.

fn main() {
    println!("cargo:rustc-link-arg=-rdynamic");
}

Hello World

After you are done installing the required crates, you can start developing your first game or application using bevy_scriptum.

To start using the library you need to first import some structs and traits with Rust use statements.

For convenience there is a main "prelude" module provided called bevy_scriptum::prelude and a prelude for each runtime you have enabled as a create feature.

You can now start exposing functions to the scripting language. For example, you can expose a function that prints a message to the console:

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<RubyRuntime>(|runtime| {
           runtime.add_function(
               String::from("my_print"),
               |In((x,)): In<(String,)>| {
                   println!("my_print: '{}'", x);
               },
           );
        })
        .run();
}

Then you can create a script file in assets directory called script.rb that calls this function:

my_print("Hello world!")

And spawn an entity with attached Script component with a handle to a script source file:

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<RubyRuntime>(|runtime| {
           runtime.add_function(
               String::from("my_print"),
               |In((x,)): In<(String,)>| {
                   println!("my_print: '{}'", x);
               },
           );
        })
        .add_systems(Startup,|mut commands: Commands, asset_server: Res<AssetServer>| {
            commands.spawn(Script::<RubyScript>::new(asset_server.load("script.rb")));
        })
        .run();
}

You should then see my_print: 'Hello world!' printed in your console.

Spawning scripts

To spawn a Ruby script you will need to get a handle to a script asset using bevy's AssetServer.

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;

fn my_spawner(mut commands: Commands, assets_server: Res<AssetServer>) {
    commands.spawn(Script::<RubyScript>::new(
        assets_server.load("my_script.rb"),
    ));
}
}

After they scripts have been evaled by bevy_scriptum, the entities that they've been attached to will get the Script::<RubyScript> component stripped and instead RubyScriptData component will be attached.

So to query scipted entities you could do something like:

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;

fn my_system(
    mut scripted_entities: Query<(Entity, &mut RubyScriptData)>,
) {
    for (entity, mut script_data) in &mut scripted_entities {
        // do something with scripted entities
    }
}
}

Calling Rust from Ruby

To call a rust function from Ruby first you need to register a function within Rust using builder pattern.

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<RubyRuntime>(|runtime| {
            // `runtime` is a builder that you can use to register functions
        })
        .run();
}

For example to register a function called my_rust_func you can do the following:

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<RubyRuntime>(|runtime| {
             runtime.add_function(String::from("my_rust_func"), || {
               println!("my_rust_func has been called");
             });
        })
        .run();
}

After you do that the function will be available to Ruby code in your spawned scripts.

my_rust_func

Since a registered callback function is a Bevy system, the parameters are passed to it as In struct with tuple, which has to be the first parameter of the closure.

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<RubyRuntime>(|runtime| {
             runtime.add_function(String::from("func_with_params"), |args: In<(String, i64)>| {
               println!("my_rust_func has been called with string {} and i64 {}", args.0.0, args.0.1);
             });
        })
        .run();
}

To make it look nicer you can destructure the In struct.

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<RubyRuntime>(|runtime| {
             runtime.add_function(String::from("func_with_params"), |In((a, b)): In<(String, i64)>| {
               println!("my_rust_func has been called with string {} and i64 {}", a, b);
             });
        })
        .run();
}

The above function can be called from Ruby

func_with_params("abc", 123)

Return value via promise

Any registered rust function that returns a value will retrurn a promise when called within a script. By calling :and_then on the promise you can register a callback that will receive the value returned from Rust function.

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<RubyRuntime>(|runtime| {
             runtime.add_function(String::from("returns_value"), || {
                123
             });
        })
        .run();
}
returns_value.and_then do |value|
    puts(value) # 123
end

Calling Ruby from Rust

To call a function defined in Ruby

def on_update
end

We need to acquire RubyRuntime resource within a bevy system. Then we will be able to call call_fn on it, providing the name of the function to call, RubyScriptData that has been automatically attached to entity after an entity with script attached has been spawned and its script evaluated, the entity and optionally some arguments.

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;

fn call_ruby_on_update_from_rust(
    mut scripted_entities: Query<(Entity, &mut RubyScriptData)>,
    scripting_runtime: ResMut<RubyRuntime>,
) {
    for (entity, mut script_data) in &mut scripted_entities {
        // calling function named `on_update` defined in Ruby script
        scripting_runtime
            .call_fn("on_update", &mut script_data, entity, ())
            .unwrap();
    }
}
}

We can also pass some arguments by providing a tuple or Vec as the last call_fn argument.

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;

fn call_ruby_on_update_from_rust(
    mut scripted_entities: Query<(Entity, &mut RubyScriptData)>,
    scripting_runtime: ResMut<RubyRuntime>,
) {
    for (entity, mut script_data) in &mut scripted_entities {
        scripting_runtime
            .call_fn("on_update", &mut script_data, entity, (123, String::from("hello")))
            .unwrap();
    }
}
}

They will be passed to on_update Ruby function

def on_update(a, b)
    puts(a) # 123
    puts(b) # hello
end

Interacting with bevy in callbacks

Every registered function is also just a regular Bevy system.

That allows you to do anything you would do in a Bevy system.

You could for example create a callback system function that prints names of all entities with Player component.

extern crate bevy;
extern crate bevy_ecs;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;

#[derive(Component)]
struct Player;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<RubyRuntime>(|runtime| {
            runtime.add_function(
                String::from("print_player_names"),
                |players: Query<&Name, With<Player>>| {
                    for player in &players {
                        println!("player name: {}", player);
                    }
                },
            );
        })
        .run();
}

In script:

print_player_names

You can use functions that interact with Bevy entities and resources and take arguments at the same time. It could be used for example to mutate a component.

extern crate bevy;
extern crate bevy_ecs;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;

#[derive(Component)]
struct Player {
    health: i32
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<RubyRuntime>(|runtime| {
            runtime.add_function(
                String::from("hurt_player"),
                |In((hit_value,)): In<(i32,)>, mut players: Query<&mut Player>| {
                    let mut player = players.single_mut().unwrap();
                    player.health -= hit_value;
                },
            );
        })
        .run();
}

And it could be called in script like:

hurt_player(5)

Builtin types

bevy_scriptum provides following types that can be used in Ruby:

  • Bevy::Vec3
  • Bevy::Entity

Bevy::Vec3

Class Methods

  • new(x, y, z)
  • current

Instance Methods

  • x
  • y
  • z

Example Ruby usage

my_vec = Bevy::Vec3.new(1, 2, 3)
set_translation(entity, my_vec)

Example Rust usage

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<RubyRuntime>(|runtime| {
             runtime.add_function(String::from("set_translation"), set_translation);
        })
        .run();
}

fn set_translation(
    In((entity, translation)): In<(BevyEntity, BevyVec3)>,
    mut entities: Query<&mut Transform>,
) {
    let mut transform = entities.get_mut(entity.0).unwrap();
    transform.translation = translation.0;
}

Bevy::Entity

Bevy::Entity.current is currently not available within promise callbacks.

Constructor

None - instances can only be acquired by using Bevy::Entity.current

Class method

  • index

Example Ruby usage

puts(Bevy::Entity.current.index)
pass_to_rust(Bevy::Entity.current)

Example Rust usage

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<RubyRuntime>(|runtime| {
             runtime.add_function(String::from("pass_to_rust"), |In((entity,)): In<(BevyEntity,)>| {
               println!("pass_to_rust called with entity: {:?}", entity);
             });
        })
        .run();
}

Rhai

This chapter demonstrates how to work with bevy_scriptum when using Rhai language runtime.

Installation

Add the following to your Cargo.toml:

[dependencies]
bevy = "0.16"
bevy_scriptum = { version = "0.9", features = ["rhai"] }

If you need a different version of bevy you need to use a matching bevy_scriptum version according to the bevy support matrix

Multiple plugins

It is possible to split the definition of your callback functions up over multiple plugins. This enables you to split up your code by subject and keep the main initialization light and clean. This can be accomplished by using add_scripting_api. Be careful though, add_scripting has to be called before adding plugins.

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

struct MyPlugin;
impl Plugin for MyPlugin {
    fn build(&self, app: &mut App) {
        app.add_scripting_api::<LuaRuntime>(|runtime| {
            runtime.add_function(String::from("hello_from_my_plugin"), || {
                info!("Hello from MyPlugin");
            });
        });
    }
}

// Main
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|_| {
            // nice and clean
        })
        .add_plugins(MyPlugin)
        .run();
}

Workflow

Demonstration of useful approaches when working with bevy_scriptum.

Live-reload

Bevy included support

To enable live reload it should be enough to enable file-watcher feature within bevy dependency in Cargo.toml

bevy = { version = "0.16", features = ["file_watcher"] }

Init-teardown pattern

It is useful to structure your application in a way that would allow making changes to the scripting code without restarting the application.

A useful pattern is to hava three functions "init", "update" and "teardown".

  • "init" function will take care of starting the application(spawning the player, the level etc)

  • "update" function will run the main application logic

  • "teardown" function will despawn all the entities so application starts at fresh state.

This pattern is very easy to implement in bevy_scriptum. All you need is to define all needed functions in script:

player = {
    entity = nil
}

-- spawning all needed entities
local function init()
	player.entity = spawn_player()
end

-- application logic here, should be called in a bevy system using call_fn
local function update()
    (...)
end

-- despawning entities and possible other cleanup logic needed
local function teardown()
	despawn(player.entity)
end

-- call init to start the application, this will be called on each file-watcher script
-- reload
init()

The function calls can be implemented on Rust side the following way:

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
use bevy_scriptum::runtimes::lua::BevyVec3;

fn init(mut commands: Commands, assets_server: Res<AssetServer>) {
    commands.spawn(Script::<LuaScript>::new(
        assets_server.load("scripts/game.lua"),
    ));
}


fn update(
    mut scripted_entities: Query<(Entity, &mut LuaScriptData)>,
    scripting_runtime: ResMut<LuaRuntime>,
) {
    for (entity, mut script_data) in &mut scripted_entities {
        scripting_runtime
            .call_fn("update", &mut script_data, entity, ())
            .unwrap();
    }
}


fn teardown(
    mut ev_asset: EventReader<AssetEvent<LuaScript>>,
    scripting_runtime: ResMut<LuaRuntime>,
    mut scripted_entities: Query<(Entity, &mut LuaScriptData)>,
) {
    for event in ev_asset.read() {
        if let AssetEvent::Modified { .. } = event {
            for (entity, mut script_data) in &mut scripted_entities {
                scripting_runtime
                    .call_fn("teardown", &mut script_data, entity, ())
                    .unwrap();
            }
        }
    }
}
}

And to tie this all together we do the following:

extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_scripting::<LuaRuntime>(|builder| {
            builder
                .add_function(String::from("spawn_player"), spawn_player)
                .add_function(String::from("despawn"), despawn);
        })
        .add_systems(Startup, init)
        .add_systems(Update, (update, teardown))
        .run();
}

fn init() {}
fn update() {}
fn despawn() {}
fn teardown() {}
fn spawn_player() {}

despawn can be implemented as:

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate bevy_scriptum;

use bevy::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

fn despawn(In((entity,)): In<(BevyEntity,)>, mut commands: Commands) {
    commands.entity(entity.0).despawn();
}
}

Implementation of spawn_player has been left out as an exercise for the reader.

Bevy support matrix

bevy versionbevy_scriptum version
0.160.8-0.9
0.150.7
0.140.6
0.130.4-0.5
0.120.3
0.110.2
0.100.1