1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329
//! ⚠️ **Pre-release, alpha version**: API is bound to change, bugs are to be expected.
//!
//! bevy_scriptum is a a plugin for [Bevy](https://bevyengine.org/) that allows you to write some of your game logic in a scripting language.
//! Currently, only [Rhai](https://rhai.rs/) is supported, but more languages may be added in the future.
//!
//! It'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 your game.
//!
//! All you need to do is register callbacks on your Bevy app like this:
//! ```rust
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//!
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_plugins(ScriptingPlugin::default())
//! .add_script_function(String::from("hello_bevy"), || {
//! println!("hello bevy, called from script");
//! });
//! ```
//! And you can call them in your scripts like this:
//! ```rhai
//! 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:
//!
//! ```rust
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//!
//! #[derive(Component)]
//! struct Player;
//!
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_plugins(ScriptingPlugin::default())
//! .add_script_function(
//! String::from("print_player_names"),
//! |players: Query<&Name, With<Player>>| {
//! for player in &players {
//! println!("player name: {}", player);
//! }
//! },
//! );
//! ```
//!
//! You can also pass arguments to your callback functions, just like you would in a regular Bevy system - using `In` structs with tuples:
//! ```rust
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//! use rhai::ImmutableString;
//!
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_plugins(ScriptingPlugin::default())
//! .add_script_function(
//! String::from("fun_with_string_param"),
//! |In((x,)): In<(ImmutableString,)>| {
//! println!("called with string: '{}'", x);
//! },
//! );
//! ```
//! which you can then call in your script like this:
//! ```rhai
//! fun_with_string_param("Hello world!");
//! ```
//!
//! ## Usage
//!
//! Add the following to your `Cargo.toml`:
//!
//! ```toml
//! [dependencies]
//! bevy_scriptum = "0.2"
//! ```
//!
//! or execute `cargo add bevy_scriptum` from your project directory.
//!
//! Add the following to your `main.rs`:
//!
//! ```rust
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//!
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_plugins(ScriptingPlugin::default())
//! .run();
//! ```
//!
//! You can now start exposing functions to the scripting language. For example, you can expose a function that prints a message to the console:
//!
//! ```rust
//! use rhai::ImmutableString;
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//!
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_plugins(ScriptingPlugin::default())
//! .add_script_function(
//! String::from("my_print"),
//! |In((x,)): In<(ImmutableString,)>| {
//! println!("my_print: '{}'", x);
//! },
//! );
//! ```
//!
//! Then you can create a script file in `assets` directory called `script.rhai` that calls this function:
//!
//! ```rhai
//! my_print("Hello world!");
//! ```
//!
//! And spawn a `Script` component with a handle to a script source file`:
//!
//! ```rust
//! use bevy::prelude::*;
//! use bevy_scriptum::Script;
//!
//! App::new()
//! .add_systems(Startup,|mut commands: Commands, asset_server: Res<AssetServer>| {
//! commands.spawn(Script::new(asset_server.load("script.rhai")));
//! });
//! ```
//!
//! ## Provided examples
//!
//! You can also try running provided examples by cloning this repository and running `cargo run --example <example_name>`. For example:
//!
//! ```bash
//! cargo run --example hello_world
//! ```
//! The examples live in `examples` directory and their corresponding scripts live in `assets/examples` directory within the repository.
//!
//! ## Bevy compatibility
//!
//! | bevy version | bevy_scriptum version |
//! |--------------|----------------------|
//! | 0.11 | 0.2 |
//! | 0.10 | 0.1 |
//!
//! ## Promises - getting return values from scripts
//!
//! Every function called from script returns a promise that you can call `.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:
//!
//! ```rhai
//! get_player_name().then(|name| {
//! print(name);
//! });
//! ```
//!
//! ## 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()` method that returns bevy entity index.
//! It is useful for accessing entity's components from scripts.
//! It can be used in the following way:
//! ```rhai
//! print("Current entity index: " + entity.index());
//! ```
//!
//! ## 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)
mod assets;
mod callback;
mod components;
mod promise;
mod systems;
use std::sync::{Arc, Mutex};
pub use crate::components::{Script, ScriptData};
pub use assets::RhaiScript;
use bevy::prelude::*;
use callback::{Callback, RegisterCallbackFunction};
use rhai::{CallFnOptions, Dynamic, Engine, EvalAltResult, FuncArgs, ParseError};
use systems::{init_callbacks, init_engine, log_errors, process_calls};
use thiserror::Error;
use self::{
assets::RhaiScriptLoader,
systems::{process_new_scripts, reload_scripts},
};
const ENTITY_VAR_NAME: &str = "entity";
/// An error that can occur when internal [ScriptingPlugin] systems are being executed
#[derive(Error, Debug)]
pub enum ScriptingError {
#[error("script runtime error: {0}")]
RuntimeError(#[from] Box<EvalAltResult>),
#[error("script compilation error: {0}")]
CompileError(#[from] ParseError),
#[error("no runtime resource present")]
NoRuntimeResource,
#[error("no settings resource present")]
NoSettingsResource,
}
#[derive(Default)]
pub struct ScriptingPlugin;
impl Plugin for ScriptingPlugin {
fn build(&self, app: &mut App) {
app.add_asset::<RhaiScript>()
.init_asset_loader::<RhaiScriptLoader>()
.init_resource::<Callbacks>()
.insert_resource(ScriptingRuntime::default())
.add_systems(Startup, init_engine.pipe(log_errors))
.add_systems(
Update,
(
reload_scripts,
process_calls.pipe(log_errors).after(process_new_scripts),
init_callbacks.pipe(log_errors),
process_new_scripts.pipe(log_errors).after(init_callbacks),
),
);
}
}
#[derive(Resource, Default)]
pub struct ScriptingRuntime {
engine: Engine,
}
impl ScriptingRuntime {
/// Get a mutable reference to the internal [rhai::Engine].
pub fn engine_mut(&mut self) -> &mut Engine {
&mut self.engine
}
/// Call a function that is available in the scope of the script.
pub fn call_fn(
&mut self,
function_name: &str,
script_data: &mut ScriptData,
entity: Entity,
args: impl FuncArgs,
) -> Result<(), ScriptingError> {
let ast = script_data.ast.clone();
let scope = &mut script_data.scope;
scope.push(ENTITY_VAR_NAME, entity);
let options = CallFnOptions::new().eval_ast(false);
let result =
self.engine
.call_fn_with_options::<Dynamic>(options, scope, &ast, function_name, args);
scope.remove::<Entity>(ENTITY_VAR_NAME).unwrap();
if let Err(err) = result {
match *err {
rhai::EvalAltResult::ErrorFunctionNotFound(name, _) if name == function_name => {}
e => Err(Box::new(e))?,
}
}
Ok(())
}
}
/// An extension trait for [App] that allows to register a script function.
pub trait AddScriptFunctionAppExt {
fn add_script_function<
Out,
Marker,
A: 'static,
const N: usize,
const X: bool,
R: 'static,
const F: bool,
Args,
>(
&mut self,
name: String,
system: impl RegisterCallbackFunction<Out, Marker, A, N, X, R, F, Args>,
) -> &mut Self;
}
/// A resource that stores all the callbacks that were registered using [AddScriptFunctionAppExt::add_script_function].
#[derive(Resource, Default)]
struct Callbacks {
uninitialized_callbacks: Vec<Callback>,
callbacks: Mutex<Vec<Callback>>,
}
impl AddScriptFunctionAppExt for App {
fn add_script_function<
Out,
Marker,
A: 'static,
const N: usize,
const X: bool,
R: 'static,
const F: bool,
Args,
>(
&mut self,
name: String,
system: impl RegisterCallbackFunction<Out, Marker, A, N, X, R, F, Args>,
) -> &mut Self {
let system = system.into_callback_system(&mut self.world);
let mut callbacks_resource = self.world.resource_mut::<Callbacks>();
callbacks_resource.uninitialized_callbacks.push(Callback {
name,
system: Arc::new(Mutex::new(system)),
calls: Arc::new(Mutex::new(vec![])),
});
self
}
}
pub mod prelude {
pub use crate::{AddScriptFunctionAppExt, ScriptingPlugin};
}