Lua Godot Bridge

Embeds Lua 5.4 into Godot 4 via GDExtension. Built from scratch for modding support in private game projects.

Why

Lua Godot Bridge

I wanted modding support in some private Godot game projects, and I built this from scratch specifically to understand GDExtension rather than drop in an existing solution. GDExtension is Godot 4's native plugin system — it lets you write shared libraries in C or C++ that integrate directly into the engine as first-class nodes and classes, no engine recompile required. Lua is a natural fit for game scripting: small, embeddable, fast enough, and widely understood by modders.

The implementation is C++ using the godot-cpp bindings submodule — GDExtension classes have to inherit from Godot's C++ base classes. Lua 5.4.8 is embedded directly (its source lives in the repo, which is why the language stats skew heavily toward C), and its C API calls cleanly from C++ via extern "C" linkage. No external runtime dependency. The bridge exposes a LuaBridge node that GDScript can use to load and run Lua scripts, pass data back and forth, and hook into the mod lifecycle. Mods are structured as directories with a JSON manifest and an entry script.

Architecture

The core problem is type conversion between Godot's Variant system and Lua's stack-based dynamic typing. Every value crossing the bridge goes through a conversion layer:

Variant → Lua C++
void godot_to_lua(lua_State* L, const Variant& value) {
    switch (value.get_type()) {
        case Variant::Type::STRING:
            lua_pushstring(L, value.operator String().utf8().get_data());
            break;
        case Variant::Type::INT:
            lua_pushinteger(L, value.operator int64_t());
            break;
        case Variant::Type::FLOAT:
            lua_pushnumber(L, value.operator double());
            break;
        case Variant::Type::ARRAY:
            push_array_to_lua(L, value.operator Array());
            break;
        case Variant::Type::DICTIONARY:
            push_dict_to_lua(L, value.operator Dictionary());
            break;
        default:
            lua_pushnil(L);
    }
}

Mods are loaded from a directory, each with a JSON manifest describing the mod and pointing to an entry script. The lifecycle mirrors Godot's own node lifecycle — on_init, on_ready, on_update, on_exit:

Mod manifest JSON
{
    "name": "ExampleMod",
    "version": "1.0.0",
    "author": "Example Author",
    "description": "An example mod",
    "entry_script": "main.lua",
    "enabled": true,
    "dependencies": []
}
Mod lifecycle hooks Lua
function on_init()
    print("initializing")
end

function on_ready()
    print("ready")
end

function on_update(delta)
    -- called every frame
end

function on_exit()
    print("shutting down")
end

Usage

GDScript interface GDScript
var lua_bridge = LuaBridge.new()
lua_bridge.initialize()

# Run a Lua string
lua_bridge.exec_string("x = 42")

# Exchange globals
lua_bridge.set_global("game_time", 123.45)
var t = lua_bridge.get_global("game_time")

# Call a Lua function
var result = lua_bridge.call_function("calculate_damage", [100, 1.5, 0.2])

# Load all mods from a directory
lua_bridge.load_mods_from_directory("res://mods/")

# Enable/disable at runtime
lua_bridge.enable_mod("ExampleMod")
lua_bridge.disable_mod("DebugMod")
lua_bridge.reload_mod("ExampleMod")

Godot objects can be accessed from Lua side through the bridge functions registered at init:

Godot object access from Lua Lua
local player = get_node("/root/Main/Player")
if player then
    call_method(player, "set_position", {100, 200})
    local health = get_property(player, "health")
    connect_signal(player, "damage_taken", "on_player_damaged")
end

function on_player_damaged(amount)
    print("took", amount, "damage")
end

Build

CMake CMake
# Lua as static library
add_library(lua STATIC ${LUA_SOURCES})
target_include_directories(lua PUBLIC ${LUA_INCLUDE_DIR})

# GDExtension shared library
add_library(lua_bridge SHARED ${EXTENSION_SOURCES})
target_include_directories(lua_bridge PRIVATE
    ${GODOT_CPP_INCLUDE_DIR}
    ${GODOT_CPP_GEN_INCLUDE_DIR}
    ${LUA_INCLUDE_DIR}
)
target_link_libraries(lua_bridge lua godot-cpp)
Build scripts Shell
# Initialize submodules first
git submodule update --init --recursive

# SCons (primary)
scons

# Or CMake
mkdir build && cd build && cmake .. && cmake --build .

# Windows
.\build.bat

Dependencies: Lua 5.4.8 (embedded), godot-cpp submodule (Godot 4.4), CMake or SCons. Windows primary; pre-built DLLs included in demo/bin.