Writing Bindings
The bevy bindings shipped with BMS are hand written, using the #[script_bindings] macro (see Controlling Script Bindings).
They live in crates/bindings/bevy_mod_scripting_bevy_<category>_bindings, one crate per area of functionality (i.e. math, ecs, ui) rather than one per upstream bevy crate.
The aim is a small, well documented scripting surface, which reads naturally from a script and compiles quickly.
Namespaces
Scripts reach types through the namespace of their category, i.e. import_namespace("math").Vec3 (see Namespaces). So each bindings crate:
- passes
namespace = "<category>"to every#[script_bindings]attribute, except onimpl Worldblocks and on functions shared by all types (impl StaticTypeRef), which would otherwise move those BMS types into the category - calls
set_crate_script_namespacesfrom its plugin’sfinish, with the bevy crates it covers, so their types without bindings land in the same namespace
What to bind
Every reflected type is already usable from scripts without any bindings:
- fields are read and written through references, i.e.
transform.translation.x = 2 - every type can be constructed with
Type.new({...}), see Constructing Arbitrary Types - enum variants are available as
Type.Variant - containers support
len,iter,pushetc., and every value can be printed
So only bind what scripts can’t already do, or can’t do comfortably:
- methods with behaviour, i.e.
Vec3:normalize()orTransform:look_at(...) - constructors for types which are built often (vectors, colors), or whose fields are private (i.e.
Name), or whose meaning isn’t obvious from their fields (i.e.Quat.from_rotation_y) - operators, by registering functions named
add,sub,mul,div,rem,neg,pow,eqorlt. With a number on the left, i.e.2 * v,addandmulare called with the operands swapped, the others callrsub,rdiv,rremorrpowon the right operand, which you register where they make sense
Do not write getters or setters for public fields.
Performance
- Take non primitive arguments as
R<T>orM<T>. These are a downcast of the reference the script holds, whileV<T>rebuilds the value through reflection. Clone natively if you need ownership. - Return a reference with an extended path when returning a part of the receiver; it costs nothing, and writes to it reach the original value. Returning
V<T>allocates a new value. - There is one function per name: registering a name again replaces the function. For mixed operands accept a
Union, i.e.rhs: Union<R<Vec3>, f32>. - Mutate in place by taking
M<Self>; pure functions return new values.
Documentation
Doc comments end up in the generated docs and in editor declaration files, and are read by script writers rather than Rust developers:
- start with one short sentence, and only describe the arguments when they are not obvious
- avoid Rust specific terms, i.e. say “or nil” rather than
Option - link bevy types using their full Rust path, i.e.
[`Vec3`](glam::Vec3), these will be resolved to the bevy docs
Examples
Keep examples short and only add them where they help. Write one block per language, one after the other, using the multi-lang-example code block attribute followed by the language:
```multi-lang-example,lua
local Vec3 = import_namespace("math").Vec3
local v = Vec3.new(1, 2, 3)
assert(v.x == 1)
```
```multi-lang-example,rhai
let Vec3 = import_namespace.call("math").Vec3;
let v = Vec3.new_.call(1.0, 2.0, 3.0);
assert(v.x == 1.0);
```
which renders as a single block with a tab per language:
local Vec3 = import_namespace("math").Vec3
local v = Vec3.new(1, 2, 3)
assert(v.x == 1)
let Vec3 = import_namespace.call("math").Vec3;
let v = Vec3.new_.call(1.0, 2.0, 3.0);
assert(v.x == 1.0);
Examples are tests: they must run as written and check their own results with assert.
Every multi-lang-example block in the doc comments under crates/ and in the book is run by the doc_example_tests test, with the scenario in assets/doc_examples/default.bmsscenario (the same app as the script tests):
cargo test --test doc_example_tests --features lua54,rhai,bevy_math_bindings -- math
Examples of a bindings crate whose feature is disabled are ignored.
If an example needs a different setup, i.e. a window, name a scenario with the scenario attribute, i.e. multi-lang-example,lua,scenario=window, which refers to assets/doc_examples/window.bmsscenario.
Mark examples which can’t run in a test app with ignore, i.e. multi-lang-example,lua,ignore, and say what they need in the surrounding docs.
Tests
Each bindings crate has its script tests under assets/tests/bindings/<category>/, with a Lua and a Rhai script per type or topic, i.e. assets/tests/bindings/math/vec3.lua and assets/tests/bindings/math/vec3.rhai.
Run them with:
cargo test --test script_tests --features lua54,rhai -- bindings/math