Game scripting
Embed omniLua to script your game in Lua. Because it's pure Rust, the scripting comes along when the game ships to the browser — which a C-backed Lua can't do.
Why Lua scripting that runs on the web
Most Rust game engines reach for a C-backed Lua (mlua, usually with LuaJIT) for scripting. Those can't compile to wasm32-unknown-unknown, so a game that targets the browser has to drop Lua or switch scripting languages for the web build.
omniLua is written in Rust and compiles to the same target your game does. The Lua you script with on desktop is the Lua that runs in the browser — one scripting layer, every platform.
Add omniLua
omnilua = "0.4"
Expose game actions to Lua
Implement UserData on a type to give Lua methods that act on it. Here a small game exposes spawn:
use omnilua::{Lua, UserData, UserDataMethods}; struct Game { entities: Vec<String> } impl UserData for Game { fn add_methods<M: UserDataMethods<Self>>(m: &mut M) { m.add_method_mut("spawn", |_, game, name: String| { game.entities.push(name); Ok(()) }); } }
Lend the game to Lua for one call
Game state isn't 'static, so you can't store it in Lua globals. scope lends a &mut borrow to Lua for the duration of one call. When the scope ends, any Lua handle to it is invalidated — using it returns an error instead of dangling.
let lua = Lua::new(); let mut game = Game { entities: vec![] }; lua.scope(|s| { let g = s.create_userdata_ref_mut(&lua, &mut game)?; lua.globals().set("game", &g)?; lua.load(r#"game:spawn("goblin")"#).exec() })?; // `game.entities` now contains "goblin"
Run a script each frame
In your update step, compile the script once and run it each frame with the world lent in. Use Chunk::into_function to parse a script a single time and call it repeatedly:
// once, at load: let tick = lua.load(player_script).into_function()?; // each frame: lua.scope(|s| { let g = s.create_userdata_ref_mut(&lua, &mut game)?; lua.globals().set("game", &g)?; tick.call::<_, ()>(()) })?;
Ship to the browser
Build for the web the same way you build any wasm Rust game. The Lua runtime is already pure Rust, so nothing extra is needed for scripting:
$ cargo build --target wasm32-unknown-unknown --release
The in-repo Bevy example drives an entity from a Lua script each frame and builds to wasm.
Player mods and untrusted scripts
If players can supply their own scripts, run them in a sandbox with CPU and memory limits and the standard library removed. See Sandboxing scripts.
Open the playground and paste a Lua snippet to see it run across all five versions.