16 releases (7 breaking)
0.8.0 | Dec 3, 2024 |
---|---|
0.8.0-alpha.1 | Nov 10, 2024 |
0.7.1 | Nov 3, 2024 |
0.6.0 | Apr 4, 2024 |
0.1.2 | Oct 8, 2022 |
#56 in Game dev
467 downloads per month
385KB
10K
SLoC
Bevy Scripting
Although Bevy doesn't directly support scripting, efforts are underway to incorporate it. This crate represents an initial attempt to enable scripting within Bevy's existing framework. It's important to note that this is a work in progress and not yet optimized or complete. As Bevy evolves, significant changes to this API are anticipated.
For a detailed look at how this crate works see architecture.md
Why Use Scripts?
- Refresh your game mechanics without the need for full crate recompilation
- Encapsulating game logic in scripts paves way for modders to create custom content easilly
- Scripting game logic/UI in a simpler language broadens development accessibility to non-programmers on your team
Features
- Hot re-loading scripts
- Lua, Teal, Rhai and Rune integrations
- Automatically generated Bevy bindings for Lua
- CLI rustc extensions for generating your own Lua bindings
- Event based hooks (i.e.
on_update
) - Flexible event scheduling (i.e. allow handling events at handling stages based on the event)
- Multiple scripts per entity
- Multiple instances of the same script on one entity
- Extensive callback argument type support
- Utilities for generating script native documentation
- Loading external lua libraries via
require
(enabled withunsafe_lua_modules
cargo feature due to potential unsafety)
Support
Support for languages is expressed in three levels:
ScriptHost
implementation, scripts can be loaded, scheduled and run in this language with support for customAPIProvider
- A Bevy API Providedr is implemented which enables access to
entity
,world
etc and provides support for at least basic operations such asget_component
,add_component
,spawn
etc - Macros for generating proxy wrapper structures exist and can be used for custom types with the ability to add script-side functionality
- Macros instantiations are automatically generated for native Bevy structures
The languages currently supported are as follows:
Language | Support Level | Documentation Generation |
---|---|---|
Lua | 4 | Yes |
Lua51 | 4 | Yes |
Lua52 | 4 | Yes |
Lua53 | 4 | Yes |
Lua54 | 4 | Yes |
Luajit | 4 | Yes |
Luajit52 | 4 | Yes |
Luau | 4 | Yes |
Teal | 4 | Yes |
Rhai | 2 | No |
Rune | 1 | No |
Usage
Installation
To install:
- Add this crate to your Cargo.toml file dependencies
- The crate is still in development so I recommended pinning to a git commit
- Add ScriptingPlugin to your app
- Add the ScriptHosts you plan on using (
add_script_host
,add_script_host_to_set
)- Make sure to attach it to a system set running AFTER any systems which may generate modify/create/remove script components
- Add script handlers to capture events in the priority range you're expecting (
add_script_handler_to_set
,add_script_handler
) - Add systems which generate ScriptEvents corresponding to your script host
- Add systems which add ScriptCollection components to your entities and fill them with scripts
An example can be seen below
fn main() -> std::io::Result<()> {
let mut app = App::new();
app.add_plugins(ScriptingPlugin)
.add_plugins(DefaultPlugins)
// pick and register only the hosts you want to use
// use any system set AFTER any systems which add/remove/modify script components
// in order for your script updates to propagate in a single frame
.add_script_host::<RhaiScriptHost<MyRhaiArgStruct>>(PostUpdate)
.add_script_host::<LuaScriptHost<MyLuaArgStruct>>(PostUpdate)
// the handlers should be ran after any systems which produce script events.
// The PostUpdate set is okay only if your API doesn't require the core Bevy systems' commands
// to run beforehand.
// Note, this setup assumes a single script handler system set with all events having identical
// priority of zero (see examples for more complex scenarios)
.add_script_handler::<LuaScriptHost<MyLuaArg>, 0, 0>(
CoreSet::PostUpdate,
)
.add_script_handler::<RhaiScriptHost<RhaiEventArgs>, 0, 0>(
CoreSet::PostUpdate,
)
// generate events for scripts to pickup
.add_system(trigger_on_update_lua)
.add_system(trigger_on_update_rhai)
// attach script components to entities
.add_startup_system(load_a_script);
app.run();
Ok(())
}
Firing Script Callbacks
Scripts are activated by dispatching ScriptEvents
. This crate employs custom priority event writers and readers, which means events are transmitted with an associated priority. This priority, in conjunction with your event pipeline, influences the sequence in which your events are processed. A priority of 0 is considered the highest.
This mechanism can be utilized to construct game loops similar to those found in Unity or other game engines.
An example event dispatching system can be seen below:
use bevy::prelude::*;
use bevy_mod_scripting::prelude::*;
// event callback generator for lua
#[cfg(feature = "lua")]
pub fn trigger_on_update_lua(mut w: PriorityEventWriter<LuaEvent<()>>) {
let event = LuaEvent::<()> {
hook_name: "on_update".to_string(),
args: (),
recipients: Recipients::All
};
w.send(event,0);
}
Adding scripts
A script is composed of:
- A reference to its code file, represented as an asset handle
- A name, typically the path relative to the assets folder
Scripts are associated with entities through bevy_mod_scripting::ScriptCollection
components, as illustrated below:
use std::sync::Mutex;
use bevy::prelude::*;
use bevy_mod_scripting::prelude::*;
// An example of a startup system which loads the lua script "console_integration.lua"
// placed in "assets/scripts/" and attaches it to a new entity
#[cfg(feature = "lua")]
pub fn load_a_script(
server: Res<AssetServer>,
mut commands: Commands,
) {
// this handle is kept by the script so it will not be unloaded
let path = "scripts/console_integration.lua".to_string();
let handle = server.load::<LuaFile>(&path);
commands.spawn(()).insert(ScriptCollection::<LuaFile> {
scripts: vec![Script::<LuaFile>::new(
path, handle,
)],
});
}
Defining an API
To make an API accessible to your scripts, you need to implement the APIProvider
trait. This can be registered with your script host using the add_api_provider
method of App
. APIProviders
function similarly to plugins:
use ::std::sync::Mutex;
use bevy_mod_scripting::prelude::*;
#[cfg(feature = "lua")]
#[derive(Default)]
pub struct LuaAPI;
#[cfg(feature = "lua")]
impl APIProvider for LuaAPI {
type APITarget = Mutex<Lua>;
type DocTarget = LuaDocFragment;
type ScriptContext = Mutex<Lua>;
fn attach_api(&mut self, ctx: &mut Self::APITarget) -> Result<(),ScriptError> {
// ... access the lua context here when the script loads
Ok(())
}
}
Register your API providers like so:
app.add_plugins(DefaultPlugins)
.add_plugins(ScriptingPlugin)
.add_script_host::<LuaScriptHost<MyLuaArg>>(PostUpdate)
.add_api_provider::<LuaScriptHost<MyLuaArg>>(Box::new(LuaAPI))
//...
The APIProvider
interface also includes setup_script
and get_doc_fragment
methods. By default, these methods do not perform any operation. However, they can be utilized for specific purposes. For instance, get_doc_fragment
can be used to generate documentation (refer to examples), and setup_script
can ensure a one-time setup per script, like setting up a Lua package path.
Documentation Generation
Documentation features are exposed at runtime via the update_documentation
builder trait method for App
:
use bevy::prelude::*;
use bevy_mod_scripting::prelude::*;
fn main() -> std::io::Result<()> {
let mut app = App::new();
app.add_plugins(DefaultPlugins)
.add_plugins(ScriptingPlugin);
#[cfg(feature = "lua")]
{
app.add_script_host::<LuaScriptHost<()>>(PostUpdate)
// Note: This is a noop in optimized builds unless the `doc_always` feature is enabled!
// this will pickup any API providers added *BEFOREHAND* like this one
.add_api_provider::<LuaScriptHost<()>>(Box::new(LuaBevyAPIProvider))
.add_api_provider::<LuaScriptHost<()>>(Box::new(LuaCoreBevyAPIProvider))
.update_documentation::<LuaScriptHost<()>>()
.add_script_handler::<LuaScriptHost<()>, 0, 0>(PostUpdate);
}
Ok(())
}
Lua
tealr
, a wrapper around the mlua
crate, provides mechanisms for Lua documentation generation. It can generate d.tl
files for static typing in Lua via the teal
project, but using teal
isn't necessary for documentation generation.
See this example for a demonstration.
The Bevy API documentation for this crate is auto-generated with each release and can be found here and here. You may need to adjust the page_root
in the auto-generated assets/doc/tealr_doc_gen_config.json
file to a path like assets/doc/YourAPI
.
Teal - Lua static typing
Teal is the recommended way of introducing lua to your bevy game. This functionality is locked behind the teal
cargo feature however, since it's quite opinionanted when it comes to your asset structure (script
and scripts/build
, folders under assets
), and also requires lua
+ teal
+ tealr_doc_gen
(cargo install --git https://github.com/lenscas/tealr_doc_gen --rev 91afd4a528e7f5b746ac3a6b299c422b42c05db6
) to be installed (see https://github.com/teal-language/tl and tealr
).
Once enabled, .tl
files can be loaded as lua scripts in addition to .lua
files and compiled on the fly. With full hot-reloading support. When you're ready to release your game, you just need to run tl build
from the assets/scripts
directory to compile your teal files. This will generate .lua
files under assets/scripts/build
. You can manage loading scripts using the bevy_mod_scripting::lua_path
macro.
If teal
is enabled and you've added the update_documentation
step to your app, every time you run/build your app in development the following will be generated/synced: - a scripts/doc
directory containing documentation for your lua exposed API - a scripts/types
directory containing .d.tl
files for your lua IDE - a scripts/tlconfig.lua
file will be generated once if it does not yet exist - any scripts with a .tl
extension will be compiled to lua code and type checked
On optimized release builds none of this happens (no debug_asserts).
The recommended workflow is to use vscode and the official teal extension with an additional tlconfig.lua
file at the root of your workspace with the
following content:
return {
include_dir = {
"path_to_your_lib/",
}
}
Configuration
SCRIPT_DOC_DIR
- documentation is generated inassets/scripts/docs
or to the path in this ENV variable if it's set.
Examples
To see more complex applications of this library have a look at the examples:
- lua - complex game loop
- lua - event recipients
- lua - bevy API
- rhai - bevy API
- generating statically typed lua wrappers + ReflectReference system
- lua - documentation generation + lua static typing
- lua - bevy console integration
- lua - dynamic queryies
- rhai - bevy console integration
- lua - game of life with teal
- rhai - game of life
Below is a video showcasing the game_of_life example:
Compatibility
bevy_mod_scripting | bevy |
---|---|
0.7 | 0.14 |
0.6 | 0.13.1 |
Dependencies
~54–89MB
~1.5M SLoC