Hello! Welcome to the final module of our course, "Lua Modding Framework & Polish."
In our previous lesson, we built a fully functional save/load system, allowing us to persist and restore the entire game world. This was a massive step, giving our simulation engine a solid, stable foundation. Now that the core of our game is robust, it's time to open it up to the creativity of others—and ourselves—through modding.
This lesson marks the beginning of that journey. Our learning outcome is to integrate a Lua scripting engine (e.g., mlua) into the Rust backend. We will build the fundamental bridge that allows our compiled Rust engine to run and interact with external Lua scripts. This is the cornerstone of the powerful, moddable architecture you envisioned for your game.
1. Why Embed a Scripting Language?
Before we dive into the code, let's briefly touch on the 'why'. For a game like ours, which aims for deep systems and full modding support, embedding a scripting language like Lua is essential. It allows game logic, content, and behaviors to be defined in simple text files that can be changed without recompiling the entire engine. This empowers a modding community and enables rapid iteration during development.
To start, watch this short clip from javidx9. Although he uses C++, his explanation for why you'd embed a language like Lua in a game engine is universal and perfectly sets the stage for our work today.
Watch the first minute of the video (00:00 - 01:03). Focus on the core benefits he mentions: modifying a program after release and enabling a modding community.
2. Setting Up mlua in Your Rust Project
We'll use the mlua crate, which provides high-level, safe, and ergonomic bindings to the Lua interpreter. It makes communicating between Rust and Lua incredibly smooth.
First, let's add mlua to your Rust project's Cargo.toml file.
[dependencies]
# ... your other dependencies like serde, serde_json, etc.
mlua = { version = "0.9", features = ["lua54", "vendored"] }
A couple of notes on the features we enabled:
"lua54": This specifies that we want to use Lua version 5.4."vendored": This is extremely convenient. It compiles and statically links a copy of the Lua source code directly into our Rust binary. This means you don't need to worry about having Lua installed separately on your system or the end-user's system. Your game executable will be self-contained.
Now, let's see how to initialize the Lua engine and run our first script.
Rust + Lua - Why & Full Code Examples
This video, 'Rust + Lua - Why & Full Code Examples' by Jeremy Chone, will be our main guide for this lesson. We'll start with the basics: setting up the crate and executing a simple inline script.
Watch from 00:30 to 04:32. The video will walk you through: Adding mlua to Cargo.toml (which we just did). Creating a new Lua instance with Lua::new(). Loading and executing a multi-line Lua script string using lua.load(...).exec(). Evaluating a script that returns a value with eval().
Let's put this into practice in our own project. In your Rust lib.rs, you can create a test function to verify that Lua is working.
// In lib.rs
use mlua::{Lua, Result};
fn run_lua_test() -> Result<()> {
// 1. Create a new Lua state
let lua = Lua::new();
println!("Lua engine initialized.");
// 2. Load and execute a simple Lua script from a string
lua.load(
r#"
-- This is a Lua comment
print('Hello from a Lua script!')
local x = 10
local y = 25
our_global_variable = x + y
"#,
)
.exec()?; // The '?' will propagate any errors
// 3. Retrieve a global variable that the script created
let globals = lua.globals();
let my_var: i32 = globals.get("our_global_variable")?;
println!("[Rust] Retrieved value from Lua: {}", my_var);
assert_eq!(my_var, 35);
Ok(())
}
// You can call this function from an init function to test it.
This simple example already demonstrates a powerful concept: Rust can execute Lua code, and Lua can modify the state (in this case, by creating a global variable) that Rust can then access.
3. Building the Two-Way Bridge
A true modding API requires seamless two-way communication. Rust needs to call functions defined in Lua scripts, and Lua scripts need to call functions exposed by the Rust engine.
3.1 Calling Lua Functions from Rust
This pattern is essential for implementing "hooks." Your engine will load a mod's script and then call specific functions at specific times, like on_character_spawn() or on_day_end().
Rust + Lua - Why & Full Code Examples
Let's continue with the video from Jeremy Chone. This next segment shows exactly how to define a function in a Lua script, load it in Rust, and then call it multiple times, even passing arguments.
Watch from 09:38 to 16:24. Pay close attention to: How the Lua script returns a function. How Rust receives this as a mlua::Function object. The use of function.call() to invoke the Lua function from Rust. The mechanism for passing arguments from Rust to the Lua function.
This ability to get a handle on a Lua Function and call it from Rust is the core mechanism you'll use to trigger mod-defined logic.
3.2 Calling Rust Functions from Lua
The reverse is just as important. Mods need to get information from the game or ask it to perform actions. This is done by exposing Rust functions to the Lua environment. A mod might call game.get_player_money() or game.create_object("chair").
Rust + Lua - Why & Full Code Examples
The final segment of the video demonstrates how to expose Rust functions so they can be called from Lua scripts. This is how you will build your game's scripting API.
Watch from 16:24 to 21:57. Focus on: The signature of a Rust function that can be called by Lua: it takes a &Lua context and arguments, and returns a mlua::Result. Using lua.create_function() to wrap your Rust logic. Using globals.set() to make the Rust function available to Lua scripts under a specific name. A good practice shown is namespacing your API functions inside a table (e.g., utils.compute_stuff()). We will use this for our game API.
This two-way communication is the heart of your modding framework. With these two patterns, you can build almost any kind of scripting API you can imagine.
4. Practical Implementation: A Basic Mod Loader
Let's build a minimalist "mod loader" to solidify these concepts. Our goal is to create a system in Rust that can:
- Initialize a Lua state.
- Expose a simple
game.register_trait()function to Lua. - Load a
test_mod.luafile from disk. - Execute the script, which will call our Rust function.
- Verify in Rust that the trait was successfully registered.
First, create a new file in your project directory called mods/test_mod.lua. (You'll need to create the mods folder).
mods/test_mod.lua
print('[Lua] test_mod.lua is running!')
-- This is a call to a function we will define in Rust and expose
-- under the 'game' table.
game.register_trait("Super-Speed")
game.register_trait("Genius")
print('[Lua] Finished registering custom traits.')
Now, let's write the Rust code to load and run this. You can add this to a new module, e.g., modding.rs, and call it from lib.rs.
modding.rs (or similar)
use mlua::{Lua, Result};
use std::fs;
use std::sync::Mutex;
use lazy_static::lazy_static; // Add `lazy_static = "1.4.0"` to Cargo.toml
// We'll store our registered traits in a global, thread-safe vector.
lazy_static! {
static ref REGISTERED_TRAITS: Mutex<Vec<String>> = Mutex::new(Vec::new());
}
pub fn initialize_and_run_mods() -> Result<()> {
let lua = Lua::new();
// --- Part 1: Expose the Rust API to Lua ---
// Create a global table called 'game' to namespace our API
let game_api = lua.create_table()?;
// Create the Rust function that Lua will call
let register_trait_func = lua.create_function(|_lua_ctx, trait_name: String| {
println!("[Rust] register_trait() called with: {}", trait_name);
// Lock the mutex and add the trait to our global list
REGISTERED_TRAITS.lock().unwrap().push(trait_name);
Ok(()) // Indicate success to Lua
})?;
// Add our Rust function to the 'game' table in Lua
game_api.set("register_trait", register_trait_func)?;
// Make the 'game' table a global variable in the Lua state
lua.globals().set("game", game_api)?;
println!("[Rust] 'game' API exposed to Lua.");
// --- Part 2: Load and Execute the Mod Script ---
let mod_path = "mods/test_mod.lua";
println!("[Rust] Loading mod script from: {}", mod_path);
let mod_code = fs::read_to_string(mod_path)
.expect("Failed to read mod file. Make sure 'mods/test_mod.lua' exists.");
lua.load(&mod_code).exec()?;
println!("[Rust] Mod script executed successfully.");
// --- Part 3: Verify the Result ---
let traits = REGISTERED_TRAITS.lock().unwrap();
println!("[Rust] All registered traits: {:?}", *traits);
assert!(traits.contains(&"Super-Speed".to_string()));
assert!(traits.contains(&"Genius".to_string()));
Ok(())
}
When you run this initialize_and_run_mods function, you will see the print statements from both Rust and Lua firing in sequence, proving that the two are communicating correctly. This simple example forms the blueprint for the entire modding system.
Conclusion
Congratulations! You have successfully integrated a Lua scripting engine into the core of your game. This is a monumental step towards achieving your goal of a deeply moddable life simulation.
Key Takeaways:
- Embedding a scripting language like Lua allows for game logic to be modified without recompiling the engine, which is key for modding and rapid development.
- The
mluacrate provides safe and ergonomic bindings for using Lua within Rust. Thevendoredfeature simplifies distribution by bundling Lua with your application. - The foundation of a modding API is two-way communication:
- Rust calls Lua: By loading a script that returns a
function, Rust can get a handle to it and invoke it. - Lua calls Rust: By using
lua.create_function()and setting it in the Lua global scope (often within a namespace table likegame), you expose your engine's functionality to scripts.
- Rust calls Lua: By loading a script that returns a
- This bridge allows you to build "hooks" and APIs, where your Rust engine can trigger script logic and scripts can query or command the engine.
Preview of the Next Lesson:
We've built the bridge. In the next lesson, "Expose a Lua API from Rust to allow modders to register custom traits," we will start sending more complex traffic across it. We'll expand on today's practical example to define how custom traits are represented in our Rust data structures and build a more robust Lua API for mods to add new character traits to the game.