Lua API Reference
Lua API Reference
Section titled “Lua API Reference”Scripts are plain Lua 5.4 files. Attach them to any node via the script_path property
in the Inspector or by using a ScriptNode / LuaComponent.
Script Lifecycle
Section titled “Script Lifecycle”local M = {}M.__index = M
function M:on_ready() -- Called once when the node enters the scene treeend
function M:on_update(dt) -- Called every frame; dt is elapsed time in secondsend
function M:on_destroy() -- Called when the node is removed from the sceneend
return MNode (self)
Section titled “Node (self)”Every lifecycle method receives self — the node the script is attached to.
-- Identityself.name -- get/set node name (string)self:type_name() -- e.g. "MeshNode"
-- Transform (Node3D and subclasses)self.position -- Vec3 get/setself.rotation -- Vec3 get/set (Euler degrees)self.scale -- Vec3 get/setself.position_x / _y / _z -- float get/set individual axesself.rotation_x / _y / _z
self:get_world_position() -- Vec3 world-space positionself:forward() / :right() / :up() -- Vec3 basis vectors
-- Hierarchyself:find("ChildName") -- Node3D or nilself:parent() -- parent Node3Dself:child_count() -- intself:get_child(index) -- Node by zero-based index
-- Tagsself:add_tag("enemy")self:has_tag("enemy") -- boolself:remove_tag("enemy")
-- Visibilityself.visible -- bool get/set
-- Componentsself:add_lua_component("scripts/my_comp.lua")local v = vec3(1, 2, 3) -- or: Vec3(1, 2, 3) — both workv.x, v.y, v.zv + other, v - other, v * scalarv:length()v:normalized()v:dot(other)v:cross(other)tostring(v) -- "Vec3(1.000, 2.000, 3.000)"-- Action names come from the project input mapInput.is_pressed("move_forward") -- held this frameInput.is_just_pressed("jump") -- became pressed this frameInput.is_just_released("attack") -- became released this frame-- Godot-style aliases also work:Input.is_action_pressed("move_forward")Input.is_action_just_pressed("jump")Input.is_action_just_released("attack")
Input.get_axis("move_left", "move_right") -- -1..1Input.get_vector("left","right","up","down") -- Vec2-like table {x,y}Input.get_mouse_delta() -- {x, y} pixelsInput.get_scroll() -- floatInput.get_strength("move_forward") -- 0..1 analogue
-- Key / mouse button raw queries (use engine: directly)engine:key_down(KEY_SPACE) -- bool; KEY_* constants availableengine:mouse_down(MOUSE_LEFT) -- bool; MOUSE_LEFT / MOUSE_RIGHT / MOUSE_MIDDLEPhysics
Section titled “Physics”Raycast
Section titled “Raycast”local hit = Physics.raycast(origin, direction, max_distance)-- Returns a table (hit may be false if nothing was struck):-- hit.hit bool-- hit.position Vec3 world-space hit point-- hit.normal Vec3 surface normal-- hit.node Node the node that was hit (nil if none)-- hit.distance float distance from origin
local hit = Physics.raycast(self:get_world_position(), self:forward(), 50.0)if hit and hit.hit then engine:log("Hit: " .. hit.node.name)end
-- Optional fifth argument: node to ignorelocal hit = Physics.raycast(origin, dir, 100, self)Sphere overlap
Section titled “Sphere overlap”local bodies = Physics.overlap_sphere(center_vec3, radius)for i, body in ipairs(bodies) do print(body.name)endRigidBody3D
Section titled “RigidBody3D”rb:get_velocity() -- Vec3rb:set_velocity(vec3(0,5,0))rb:get_angular_velocity()rb:set_angular_velocity(v)rb:apply_impulse(vec3(0,10,0))rb:apply_force(v)rb:apply_torque_impulse(v)rb:freeze_rotation(true)rb:set_kinematic(true)rb.mass -- float get/setrb.gravity_scale -- float get/setCharacterBody3D
Section titled “CharacterBody3D”cb:move_and_slide(velocity_vec3, dt) -- returns clamped velocitycb:is_on_ground() -- boolcb:get_velocity() -- Vec3Collision callbacks
Section titled “Collision callbacks”function M:on_collision_enter(other) print("Collided with " .. other.name)endfunction M:on_collision_exit(other) endArea3D Callbacks
Section titled “Area3D Callbacks”function M:on_body_entered(other) print(other.name .. " entered the trigger")endfunction M:on_body_exited(other) print(other.name .. " left the trigger")end
-- Query overlapping bodies at any timelocal bodies = area_node:get_overlapping_bodies()area_node:is_overlapping_with(other_node) -- boolarea_node:overlap_count() -- int-- Find / queryScene.get_node("NodeName") -- first match by nameScene.get_root() -- root NodeScene.find_by_tag("enemy") -- table of nodes
-- Create primitiveslocal box = Scene.create_node("MeshNode")box.name = "Crate"box.position = vec3(0, 5, 0)
-- Load a GLB as a ModelNode (auto-spawned at root)local model = Scene.instantiate_model("assets/models/barrel.glb")model.position = vec3(2, 0, 0)
-- Manually add a pending node to a parentlocal rb = engine:create_node("RigidBody3D") -- held in pending poolengine:add_node(box, rb) -- spawn under box
-- DestroyScene.destroy_node(box)
-- Load another sceneScene.load("scenes/level2.solscene")-- Attached AudioStreamPlayerlocal player = Scene.get_node("Music")player:play()player:stop()player:is_playing() -- boolplayer.volume -- float (linear)player.pitch -- floatplayer.loop -- bool
-- Fire-and-forgetAudio.play_oneshot("assets/audio/explosion.ogg")Audio.play_oneshot_bus("assets/audio/footstep.ogg", "sfx")
-- One-shot via engineengine:play_sound("assets/audio/ping.ogg")engine:play_sound_bus("assets/audio/ping.ogg", "sfx")
-- Master / bus volumeengine:set_master_volume(0.8)engine:set_bus_volume("sfx", 0.5)Log / Debug
Section titled “Log / Debug”print("hello") -- appears in the Console panelengine:log("message") -- engine log (same output)Log.info("message")Log.warn("message")Log.error("message")-- One-shotengine:create_timer(2.0, function() print("two seconds later")end)
-- Repeatinglocal id = engine:create_timer(1.0, function() print("every second")end, true)
engine:cancel_timer(id)Engine globals
Section titled “Engine globals”engine.delta_time -- float, seconds since last frameengine.elapsed_time -- float, total seconds since startengine:screen_width()engine:screen_height()engine:set_cursor_captured(true) -- FPS mouse captureengine:cursor_x() / :cursor_y() -- screen-space positionengine:quit()Full Example — Spawner Script
Section titled “Full Example — Spawner Script”local M = {}M.__index = M
local COUNT = 0
function M:on_ready() engine:log("Spawner ready. Press F to spawn a crate.")end
function M:on_update(dt) if Input.is_just_pressed("spawn_crate") then self:spawn() endend
function M:spawn() COUNT = COUNT + 1 local rb = engine:create_node("RigidBody3D") rb.name = "Crate_" .. COUNT rb.mass = 5.0 rb.position = vec3( (math.random() - 0.5) * 10, 4.0, (math.random() - 0.5) * 10 ) rb:add_tag("spawned")
engine:add_node(engine:get_root_node(), rb)
engine:create_timer(8.0, function() local found = engine:find_node(rb.name) if found then engine:destroy_node(found) end end)
engine:log("Spawned " .. rb.name)end
return MLua API Reference
Section titled “Lua API Reference”Scripts are plain Lua 5.4 files. Attach them to any node via the script_path property
in the Inspector or by using a ScriptNode / LuaComponent.
Script Lifecycle
Section titled “Script Lifecycle”local node = {}
function node.on_ready(self) -- Called once when the node enters the scene treeend
function node.on_update(self, dt) -- Called every frame; dt is elapsed time in secondsend
function node.on_destroy(self) -- Called when the node is removed from the sceneend
return nodeNode (self)
Section titled “Node (self)”Methods available on any node reference (self in lifecycle hooks, or returned by scene queries):
-- Identityself:get_name() -- stringself:set_name("Name")self:get_type() -- e.g. "MeshNode"
-- Transformself:get_position() -- Vec3self:set_position(Vec3(x, y, z))self:get_rotation() -- Vec3 (Euler degrees)self:set_rotation(Vec3(x, y, z))self:get_scale() -- Vec3self:set_scale(Vec3(x, y, z))
-- Hierarchyself:get_parent() -- Node or nilself:get_child(index) -- Nodeself:get_child_count() -- intself:set_parent(node) -- reparentself:add_child(node)
-- Enable / disableself:set_visible(bool)self:is_visible() -- bool
-- Log helperself:get_scene() -- Scene handlelocal v = Vec3(1, 2, 3)v.x, v.y, v.zVec3(0, 0, 0) -- zero constructorv + other, v - other, v * scalarv:length()v:normalized()Vec3.dot(a, b)Vec3.cross(a, b)Vec3.lerp(a, b, t)-- Action names are defined in the project's input mapInput.is_action_pressed("move_forward") -- heldInput.is_action_just_pressed("jump") -- pressed this frameInput.is_action_just_released("attack") -- released this frame
Input.get_axis("move_left", "move_right") -- returns -1..1 analogue valuePhysics
Section titled “Physics”Raycast
Section titled “Raycast”local hit = Physics.raycast(origin, direction, max_distance)-- Returns nil if no hit, or:-- hit.position Vec3 world-space hit point-- hit.normal Vec3 surface normal-- hit.node Node the node that was hit-- hit.distance float distance from origin
-- Examplelocal hit = Physics.raycast( self:get_position(), Vec3(0, -1, 0), 100.0)if hit then print("Hit: " .. hit.node:get_name() .. " at " .. hit.distance)endCollision callbacks on RigidBody3D
Section titled “Collision callbacks on RigidBody3D”function node.on_collision_enter(self, other) print("Collided with " .. other:get_name())end
function node.on_collision_exit(self, other) print("Separated from " .. other:get_name())endCharacterBody3D
Section titled “CharacterBody3D”character:move_and_slide(velocity, dt) -- returns actual velocity after slidecharacter:is_on_floor() -- boolcharacter:get_velocity() -- Vec3character:set_velocity(Vec3(x, y, z))Area3D Callbacks
Section titled “Area3D Callbacks”function node.on_body_entered(self, other) print(other:get_name() .. " entered the trigger")end
function node.on_body_exited(self, other) print(other:get_name() .. " left the trigger")end
-- Query overlapping bodies at any timelocal bodies = area_node:get_overlapping_bodies()for i, body in ipairs(bodies) do print(body:get_name())endQuerying nodes
Section titled “Querying nodes”local node = Scene.get_node("NodeName") -- by name (first match)local node = Scene.get_node_by_path("/Root/Child/NodeName")Creating nodes
Section titled “Creating nodes”-- Primitive nodelocal box = Scene.create_node("MeshNode")box:set_name("Crate")box:set_position(Vec3(0, 5, 0))box:set_mesh_type("box") -- "box" | "sphere" | "capsule" | "cylinder" | "plane"
-- Load a GLB as a ModelNodelocal model = Scene.instantiate_model("assets/models/barrel.glb")model:set_position(Vec3(2, 0, 0))
-- Attach physicslocal rb = Scene.create_node("RigidBody3D")rb:set_parent(box)
-- Destroy a nodeScene.destroy_node(box)AudioStreamPlayer / AudioStreamPlayer3D
Section titled “AudioStreamPlayer / AudioStreamPlayer3D”local player = Scene.get_node("MyAudioPlayer")player:play()player:stop()player:is_playing() -- boolplayer:set_volume_db(-6)player:get_volume_db()One-shot fire-and-forget
Section titled “One-shot fire-and-forget”Audio.play_oneshot("assets/audio/explosion.ogg")Audio.play_oneshot_3d("assets/audio/footstep.ogg", Vec3(x, y, z))Log / Debug
Section titled “Log / Debug”print("hello") -- [Lua] hello in engine logLog.info("message")Log.warn("message")Log.error("message")Full Example — Spawner Script
Section titled “Full Example — Spawner Script”local spawner = {}local count = 0
function spawner.on_ready(self) print("Spawner ready. Press F to spawn a crate.")end
function spawner.on_update(self, dt) if Input.is_action_just_pressed("spawn") then count = count + 1 local crate = Scene.instantiate_model("assets/models/crate.glb") local pos = self:get_position() + Vec3(math.random(-3, 3), 4, math.random(-3, 3)) crate:set_position(pos) crate:set_name("Crate_" .. count)
local rb = Scene.create_node("RigidBody3D") rb:set_parent(crate)
print("Spawned crate #" .. count) endend
return spawner