Godot 4.5+ GDExtension to integrate the Luau scripting language (a high-performance Lua derivative) into Godot Engine.
📖 Documentation: Full API reference is available in Godot's built-in help system. Search for LuaState, Luau, or LuauScript in the editor, or browse the XML files in doc_classes/.
Luau is a high-performance scripting language derived from Lua 5.1, originally created by Roblox. It offers several advantages for integration into games:
- Performance: Up to 2x faster than standard Lua 5.1 with optimized bytecode
- Type Checking: Optional gradual typing catches errors before runtime
- Safety: Built-in sandboxing capabilities for running untrusted code
- Modern Features: Native vector type, improved string library, buffer API
This extension is currently built against Luau 0.719, though the API only exposes features available from Luau 0.696 and earlier. (No specific reason, we just need to review the changelog!)
# Create a Lua state and load libraries
var state := LuaState.new()
state.open_libs()
# Compile and execute Luau code
var bytecode := Luau.compile("print('Hello from Luau!')")
if state.load_bytecode(bytecode, "hello"):
state.pcall(0, 0) # Execute with 0 arguments, 0 return valuesGodot math types work seamlessly in Luau with operator support:
-- Vector3 uses Luau's native vector type for maximum performance
local pos = Vector3(10, 20, 30)
local dir = Vector3(1, 0, 0)
local new_pos = pos + dir * 5.0 -- Inline VM operations, JIT-friendly
-- Built-in vector library functions
local length = vector.magnitude(pos)
local normalized = vector.normalize(dir)
-- Other math types (Vector2, Vector4, Color, etc.)
local color = Color(1.0, 0.5, 0.0, 1.0)
local tinted = color * 0.8# Pass Godot data to Luau
var player_data := {"name": "Player", "health": 100.0, "level": 5.0}
var state := LuaState.new()
state.open_libs()
state.push_variant(player_data)
state.set_global("player")
# Execute Luau code that modifies the data
state.do_string("""
player.health = player.health - 10
player.level = player.level + 1
""", "modify_player")
# Retrieve modified data back to Godot
state.get_global("player")
var updated_data := state.to_dictionary(-1)
print(updated_data) # {"name": "Player", "health": 90.0, "level": 6.0}# Pass Godot Callables to Luau
var state := LuaState.new()
state.open_libs()
state.push_variant(func(x): return x * 2)
state.set_global("double")
state.do_string("print(double(21))", "test_callable") # Prints: 42
# Get Lua functions as Godot Callables
state.do_string("function add(a, b) return a + b end", "define_add")
state.get_global("add")
var lua_add := state.to_callable(-1) # Returns a Callable
print(lua_add.call(10, 32)) # Prints: 42Sandboxing protects builtin libraries and enables runtime optimizations:
var state := LuaState.new()
state.open_libs()
# Set up any globals you want to expose
state.push_variant({"max_health": 100})
state.set_global("config")
# Load untrusted code
var untrusted_code := """
-- This code cannot modify builtin libraries
-- string.byte = nil -- This would fail
print("Player max health: " .. config.max_health)
"""
var bytecode := Luau.compile(untrusted_code)
state.load_bytecode(bytecode, "untrusted")
# Apply sandbox AFTER loading code, BEFORE executing
state.sandbox()
# Now safe to execute
state.pcall(0, 0)For threads, use sandbox_thread() after the main state is sandboxed:
# Main state already sandboxed
var thread := state.new_thread()
state.pop(1)
# Load code into thread
thread.load_bytecode(bytecode, "thread_func")
# Sandbox the thread before execution
thread.sandbox_thread()
# Now safe to execute
thread.pcall(0, 0)# Create a Lua thread (coroutine)
var state := LuaState.new()
state.open_libs()
state.do_string("""
function counter()
for i = 1, 3 do
coroutine.yield(i)
end
end
""", "define_counter")
var thread := state.new_thread()
state.pop(1) # Clean up the thread from main stack
thread.get_global("counter")
var status := thread.resume(0) # Call the function with 0 arguments
# Resume the coroutine multiple times
while status == Luau.LUA_YIELD:
print(thread.to_number(-1)) # Prints: 1, 2, 3
thread.pop(1)
status = thread.resume(0)Prerequisites:
Build the preset appropriate for your platform—for example:
cmake --preset windows-x86_64-debug
cmake --build --preset windows-x86_64-debug -jcmake --preset linux-x86_64-debug
cmake --build --preset linux-x86_64-debug -jcmake --preset macos-arm64-debug
cmake --build --preset macos-arm64-debug -jThis project has comprehensive automated tests for all Godot-Luau bridging functionality:
ctest --preset windows-x86_64-debug
ctest --preset linux-x86_64-debug
ctest --preset macos-arm64-debug📖 For detailed testing documentation, see
tests/README.md
Test coverage includes:
- ✅ Math types (Vector2/3, Color, etc.)
- ✅ Array/Dictionary bridging
- ✅ Variant conversions
- ✅ Edge cases and error handling
All Godot math types with full operator support:
- Vector2, Vector2i - 2D vectors (integer and float)
- Vector3, Vector3i - 3D vectors with native Luau vector optimization
- Vector4, Vector4i - 4D vectors
- Color - RGBA colors with arithmetic operations
- Rect2, Rect2i - 2D rectangles
- Plane, AABB - Geometric primitives
- Quaternion, Basis, Transform2D, Transform3D, Projection - Transforms
- Array - Godot Arrays (bidirectional conversion to/from Lua tables)
- Dictionary - Godot Dictionaries (bidirectional conversion to/from Lua tables)
- String - Transparent string bridging
- Callable - First-class functions crossing language boundaries
- Variant - Generic type that handles all Godot types
Run the demo project to see Luau integration in action:
godot --editor --path demo/The demo showcases:
- Compiling and executing Luau scripts
- Single-step debugging with breakpoints and step signals
- Math type usage with operators (Vector2, Vector3, Color, etc.)
- Array and Dictionary conversions (including nested structures)
- Sandboxing for safe script execution
- Vector library functions (magnitude, normalize, dot, cross, etc.)
This project is licensed under the MIT License. See the LICENSE file for details.