Script Godot objects directly in Lua or create sandboxed LuaStates for modding (Godot 4.5+)
See the codeExtension for using the Lua programming language in Godot 4.5+
With this addon, you can program your game or application directly in Lua. You can also create sandboxed Lua states for external modding/scripting support, as many as necessary.
This plugin is available in the Asset Library as:
base, package and io librariesprint_rich, lerp and is_same.
Also adds an await function that mimics GDScript's await keyword (only supported when running in a coroutine).OK, TYPE_STRING and SIDE_LEFTpackage.searchers, require, loadfile and dofile to accept paths relative to res:// and user://This addon registers a ScriptLanguageExtension so that Godot objects can be scripted directly in Lua.
For Lua scripts to be usable in Nodes and Resources, they must return a table with the script metadata containing methods, properties, signals, etc...
-- This is our script metadata table.
--
-- It stores metadata such as its base class, global class_name, icon,
-- as well as any declared properties, methods and signals
-- Using `GDCLASS` to create the table keeps properties and functions ordered
local LuaBouncingLogo = GDCLASS()
-- base class (optional, defaults to RefCounted)
LuaBouncingLogo.extends = Node2D
-- if true, allow the script to be executed by the editor (optional)
LuaBouncingLogo.tool = false
-- global class name (optional)
LuaBouncingLogo.class_name = "LuaBouncingLogo"
-- Declare properties
LuaBouncingLogo.linear_velocity = export(100)
LuaBouncingLogo.initial_angle = export_range(-360, 360, "degrees", float)
-- Declare signals
LuaBouncingLogo.bounced = signal()
-- Called when the node enters the scene tree for the first time.
function LuaBouncingLogo:_ready()
self.movement = Vector2(self.linear_velocity, 0):rotated(deg_to_rad(self.initial_angle))
-- To connect a signal in Lua, you can use the method callable just like in GDScript
self.bounced:connect(self._on_bounced)
-- Or you can use a Callable constructed from a lua function
self.ready:connect(Callable(function()
print("This works!")
end))
end
-- Called every frame. 'delta' is the elapsed time since the previous frame.
function LuaBouncingLogo:_process(delta)
local viewport_rect = self:get_viewport_rect()
local self_global_rect = self.global_transform * self:get_rect()
local bounced = false
-- bounce on viewport top
if self_global_rect.position.y <= viewport_rect.position.y then
self.movement = self.movement:reflect(Vector2.RIGHT)
bounced = true
end
-- bounce on viewport bottom
if self_global_rect["end"].y >= viewport_rect["end"].y then
self.movement = self.movement:reflect(Vector2.RIGHT)
bounced = true
end
-- bounce on viewport left
if self_global_rect.position.x <= viewport_rect.position.x then
self.movement = self.movement:reflect(Vector2.UP)
bounced = true
end
-- bounce on viewport right
if self_global_rect["end"].x >= viewport_rect["end"].x then
self.movement = self.movement:reflect(Vector2.UP)
bounced = true
end
self.position = self.position + self.movement * delta
if bounced then
self.bounced:emit()
end
end
function LuaBouncingLogo:_on_bounced()
print("Bounced =D")
end
-- Return the metadata table for the script to be usable by Godot objects
return LuaBouncingLogo
The following classes are registered in Godot for creating Lua states and interacting with them: LuaState, LuaTable, LuaUserdata, LuaLightUserdata, LuaFunction, LuaCoroutine, LuaThread, LuaDebug and LuaError.
Usage example in GDScript:
# 1. Create a Lua state
var lua = LuaState.new()
# 2. Import Lua and Godot APIs into the state
# Optionally pass which libraries should be opened to the method
lua.open_libraries()
# 3. Run Lua code using `LuaState.do_string` or `LuaState.do_file`
var result = lua.do_string("""
local vector = Vector2(1, 2)
return {
this_is_a_table = true,
vector = vector,
}
""")
# 4. Access results from Lua code directly in Godot
# When errors occur, instances of `LuaError` will be returned
if result is LuaError:
printerr("Error in Lua code: ", result)
else:
print(result) # [LuaTable:0x556069ee50ab]
print(result["this_is_a_table"]) # true
print(result["vector"]) # (1, 2)
print(result["invalid key"]) # <null>
# 5. Access the global _G table via `LuaState.globals` property
assert(lua.globals is LuaTable)
lua.globals["a_godot_callable"] = func(): print("Hello from GDScript!")
lua.do_string("""
a_godot_callable() -- 'Hello from GDScript!'
""")
local v = Vector3(1, 2, 3)
print(v.x) -- 1
-- Note: use ":" instead of "." to call methods in Lua
print(v:length()) -- 3.74165749549866
local n = Node:new()
print(n:is_inside_tree()) -- false
n:queue_free()
-- Element type: int
local int_array = Array[int]()
-- Element type: Node
local node_array = Array[Node]()
-- Key: int, Value: bool
local int_to_bool_dict = Dictionary[int][bool]()
-- Key: Variant, Value: Node
local node_valued_dict = Dictionary[Variant][Node]()
local d1 = Dictionary()
local d2 = Dictionary()
print(is_same(d1, d2)) -- false
print(is_same(d2, d2)) -- true
print(lerp(5, 10, 0.15)) -- 5.75
assert(OS == Engine:get_singleton("OS"))
local array = Array{ "value 0", "value 1" }
-- Godot Arrays are indexed from 0, instead of 1
print(array[0]) -- "value 0"
print(array[1]) -- "value 1"
print(array[2]) -- nil
local dict = Dictionary{ hello = "world" }
print(dict) -- { "hello": "world" }
print(dict["hello"]) -- "world"
print(dict["invalid key"]) -- nil
pairs for types that support it, like Arrays, packed arrays, Dictionaries and some math types.
local dictionary = Dictionary{ hello = "world", key = "value" }
for key, value in pairs(dictionary) do
print(key .. ": " .. value)
end
#) as a shortcut for calling the size() method in any object that supports it.
local array = Array{ 1, 2, 3, 4 }
print(#array) -- 4
Variant.is method.
local array = Array()
print(Variant.is(array, Array)) -- true
print(Variant.is(array, 'Array')) -- true
-- Also available using the method notation from Variant objects
print(array:is(Array)) -- true
print(array:is(Dictionary)) -- false
print(array:is(RefCounted)) -- false
pcall.
local v = Vector2(1, 2)
print(v:pcall('length')) -- true 2.2360680103302
print(v:pcall('invalid method')) -- false "Invalid method"
res:// relative paths in require, loadfile and dofileawaiting signalsexport_* functions mimicking GDScript annotations for better UX3,846 followers · starred Oct 2025
207 followers · starred Apr 2025
76 followers · starred May 2025
257 followers · starred Apr 2025
Script Godot objects directly in Lua or create sandboxed LuaStates for modding (Godot 4.5+)
See the codeExtension for using the Lua programming language in Godot 4.5+
With this addon, you can program your game or application directly in Lua. You can also create sandboxed Lua states for external modding/scripting support, as many as necessary.
This plugin is available in the Asset Library as:
base, package and io librariesprint_rich, lerp and is_same.
Also adds an await function that mimics GDScript's await keyword (only supported when running in a coroutine).OK, TYPE_STRING and SIDE_LEFTpackage.searchers, require, loadfile and dofile to accept paths relative to res:// and user://This addon registers a ScriptLanguageExtension so that Godot objects can be scripted directly in Lua.
For Lua scripts to be usable in Nodes and Resources, they must return a table with the script metadata containing methods, properties, signals, etc...
-- This is our script metadata table.
--
-- It stores metadata such as its base class, global class_name, icon,
-- as well as any declared properties, methods and signals
-- Using `GDCLASS` to create the table keeps properties and functions ordered
local LuaBouncingLogo = GDCLASS()
-- base class (optional, defaults to RefCounted)
LuaBouncingLogo.extends = Node2D
-- if true, allow the script to be executed by the editor (optional)
LuaBouncingLogo.tool = false
-- global class name (optional)
LuaBouncingLogo.class_name = "LuaBouncingLogo"
-- Declare properties
LuaBouncingLogo.linear_velocity = export(100)
LuaBouncingLogo.initial_angle = export_range(-360, 360, "degrees", float)
-- Declare signals
LuaBouncingLogo.bounced = signal()
-- Called when the node enters the scene tree for the first time.
function LuaBouncingLogo:_ready()
self.movement = Vector2(self.linear_velocity, 0):rotated(deg_to_rad(self.initial_angle))
-- To connect a signal in Lua, you can use the method callable just like in GDScript
self.bounced:connect(self._on_bounced)
-- Or you can use a Callable constructed from a lua function
self.ready:connect(Callable(function()
print("This works!")
end))
end
-- Called every frame. 'delta' is the elapsed time since the previous frame.
function LuaBouncingLogo:_process(delta)
local viewport_rect = self:get_viewport_rect()
local self_global_rect = self.global_transform * self:get_rect()
local bounced = false
-- bounce on viewport top
if self_global_rect.position.y <= viewport_rect.position.y then
self.movement = self.movement:reflect(Vector2.RIGHT)
bounced = true
end
-- bounce on viewport bottom
if self_global_rect["end"].y >= viewport_rect["end"].y then
self.movement = self.movement:reflect(Vector2.RIGHT)
bounced = true
end
-- bounce on viewport left
if self_global_rect.position.x <= viewport_rect.position.x then
self.movement = self.movement:reflect(Vector2.UP)
bounced = true
end
-- bounce on viewport right
if self_global_rect["end"].x >= viewport_rect["end"].x then
self.movement = self.movement:reflect(Vector2.UP)
bounced = true
end
self.position = self.position + self.movement * delta
if bounced then
self.bounced:emit()
end
end
function LuaBouncingLogo:_on_bounced()
print("Bounced =D")
end
-- Return the metadata table for the script to be usable by Godot objects
return LuaBouncingLogo
The following classes are registered in Godot for creating Lua states and interacting with them: LuaState, LuaTable, LuaUserdata, LuaLightUserdata, LuaFunction, LuaCoroutine, LuaThread, LuaDebug and LuaError.
Usage example in GDScript:
# 1. Create a Lua state
var lua = LuaState.new()
# 2. Import Lua and Godot APIs into the state
# Optionally pass which libraries should be opened to the method
lua.open_libraries()
# 3. Run Lua code using `LuaState.do_string` or `LuaState.do_file`
var result = lua.do_string("""
local vector = Vector2(1, 2)
return {
this_is_a_table = true,
vector = vector,
}
""")
# 4. Access results from Lua code directly in Godot
# When errors occur, instances of `LuaError` will be returned
if result is LuaError:
printerr("Error in Lua code: ", result)
else:
print(result) # [LuaTable:0x556069ee50ab]
print(result["this_is_a_table"]) # true
print(result["vector"]) # (1, 2)
print(result["invalid key"]) # <null>
# 5. Access the global _G table via `LuaState.globals` property
assert(lua.globals is LuaTable)
lua.globals["a_godot_callable"] = func(): print("Hello from GDScript!")
lua.do_string("""
a_godot_callable() -- 'Hello from GDScript!'
""")
local v = Vector3(1, 2, 3)
print(v.x) -- 1
-- Note: use ":" instead of "." to call methods in Lua
print(v:length()) -- 3.74165749549866
local n = Node:new()
print(n:is_inside_tree()) -- false
n:queue_free()
-- Element type: int
local int_array = Array[int]()
-- Element type: Node
local node_array = Array[Node]()
-- Key: int, Value: bool
local int_to_bool_dict = Dictionary[int][bool]()
-- Key: Variant, Value: Node
local node_valued_dict = Dictionary[Variant][Node]()
local d1 = Dictionary()
local d2 = Dictionary()
print(is_same(d1, d2)) -- false
print(is_same(d2, d2)) -- true
print(lerp(5, 10, 0.15)) -- 5.75
assert(OS == Engine:get_singleton("OS"))
local array = Array{ "value 0", "value 1" }
-- Godot Arrays are indexed from 0, instead of 1
print(array[0]) -- "value 0"
print(array[1]) -- "value 1"
print(array[2]) -- nil
local dict = Dictionary{ hello = "world" }
print(dict) -- { "hello": "world" }
print(dict["hello"]) -- "world"
print(dict["invalid key"]) -- nil
pairs for types that support it, like Arrays, packed arrays, Dictionaries and some math types.
local dictionary = Dictionary{ hello = "world", key = "value" }
for key, value in pairs(dictionary) do
print(key .. ": " .. value)
end
#) as a shortcut for calling the size() method in any object that supports it.
local array = Array{ 1, 2, 3, 4 }
print(#array) -- 4
Variant.is method.
local array = Array()
print(Variant.is(array, Array)) -- true
print(Variant.is(array, 'Array')) -- true
-- Also available using the method notation from Variant objects
print(array:is(Array)) -- true
print(array:is(Dictionary)) -- false
print(array:is(RefCounted)) -- false
pcall.
local v = Vector2(1, 2)
print(v:pcall('length')) -- true 2.2360680103302
print(v:pcall('invalid method')) -- false "Invalid method"
res:// relative paths in require, loadfile and dofileawaiting signalsexport_* functions mimicking GDScript annotations for better UX3,846 followers · starred Oct 2025
207 followers · starred Apr 2025
76 followers · starred May 2025
257 followers · starred Apr 2025