Lua API
Ra Engine's game logic layer. Drop a .lua script in scripts/, point the engine at it, and the full engine API is available as the global table engine. Changes to the script hot-reload live — no recompile needed.
engine global. This lets you iterate on game logic independently of the graphics pipeline.
01. Script Contract
Every Ra Engine Lua script must define these global functions. The engine calls them automatically.
Called once when the script first loads, and again on every hot-reload and after every engine.load_scene(). Use this to look up object IDs with engine.find_object() and engine.find_objects_by_prefix(), reset animation state, and read any persisted state from engine.get_var().
Do not cache object IDs across hot-reloads in module-level Lua variables — the scene may have changed. Always re-discover them inside init().
Called every frame before rendering. dt is the frame delta time in seconds (capped; safe for physics and animation). All game logic, input polling, scoring, object movement, and scale/emissive animations go here.
Called on engine exit (before Vulkan teardown). Save any game state that must survive process restart using engine.set_var(). This is your last chance to write persistent data.
Return a short string shown in the engine's terminal status area (bottom line). Called every frame. Return "" to show nothing. Keep it under ~80 characters for clean display.
02. Hot-Reload
Every frame, the engine stat()s the script file. If the modification time has changed, it automatically:
- Calls
shutdown()on the current Lua state - Destroys the old
lua_State - Opens a fresh state and re-binds the full engine API
- Runs the script file
- Calls
init()again
lua_State is torn down. Only values written to engine.set_var() survive — they live in C++ and are unaffected by Lua restarts.
If the reloaded script has a syntax or runtime error, the engine keeps the previous working state and suppresses further reload attempts until the file is saved again.
03. Persisting State
Use engine.set_var() / engine.get_var() for any value that must survive hot-reloads, scene transitions, or be accessible between update() and shutdown().
function init()
-- Read back values saved from a previous session or hot-reload
score = engine.get_var("launch_up.score", 0.0)
peak_height = engine.get_var("launch_up.peak_height", 0.0)
current_level = math.floor(engine.get_var("launch_up.level", 1.0) + 0.5)
end
function shutdown()
engine.set_var("launch_up.score", score)
engine.set_var("launch_up.peak_height", peak_height)
engine.set_var("launch_up.level", current_level)
end
"launch_up.score") to avoid collisions if multiple scripts are used across levels.
04. Camera
The camera is the player in first-person mode. Moving the camera moves the player. Physics and collision are resolved relative to the camera position.
Returns the camera's world-space position as three separate floats. Y is vertical — the primary axis for height-based scoring.
local _, cy, _ = engine.get_camera_position()
if cy > peak_height then
peak_height = cy
score = (peak_height - spawn_y) * 100.0
end
Teleports the camera (and player) to the given world-space position. Used for respawning. Always follow with engine.set_vertical_velocity(0.0) to clear falling momentum.
local cx, cy, cz = engine.get_checkpoint()
engine.set_camera_position(cx, cy, cz)
engine.set_vertical_velocity(0.0)
Returns the normalised camera look direction. Useful for shooting, raycasting, or directional movement logic.
Returns the normalised camera right vector (perpendicular to forward in the horizontal plane). Use with forward for strafe-relative logic.
05. Physics
Returns a table describing the player's current physics state.
Directly sets the player's vertical velocity. Use to trigger a jump or cancel falling momentum on respawn. Engine gravity is −18.0 m/s².
06. Input
Returns a snapshot of the current frame's input state. Read this once at the top of update().
true if the key is currently held. Use for continuous actions.true only on the single frame the key went down. Use for discrete actions (restart, next level).local inp = engine.get_input()
-- One-shot: restart or advance level
if inp.key_just_pressed[KEY_R] then
-- restart current level
elseif inp.key_just_pressed[KEY_N] then
engine.load_scene(LEVELS[current_level + 1])
end
true for exactly one frame. Always read input once at the top of update() — never inside a loop.
07. Time
Returns wall-clock seconds as a double. Use for continuous animations: emissive pulses, scale breathe effects, rotation offsets.
local t = engine.get_time()
-- Hazard breathe: scale oscillates between 0.92 and 1.08
local breathe = 1.0 + 0.08 * math.sin(t * 2.5)
for _, hid in ipairs(hazard_ids) do
engine.set_object_scale(hid, breathe)
end
-- Emissive pulse
local p = (math.sin(t * 3.0) + 1.0) * 0.5
engine.set_object_emissive(glow_id, 0.0, 1.0, 0.9, 2.0 + p * 3.0)
Returns the same dt passed to update(dt). Convenient for helper functions that don't receive dt as a parameter.
08. Scene Objects
Scene objects are identified by a uint32 ID assigned at load time. IDs must be re-acquired after any scene load or hot-reload — always call engine.find_object() and engine.find_objects_by_prefix() inside init().
engine.find_object() returns 0 when no object with that name exists. Always check before using an ID.
Transform
Looks up a scene object by its exact GLTF node name. Returns the integer ID, or 0 if not found.
function init()
goal_id = engine.find_object("goal_pad")
if goal_id == 0 then
engine.print_status("WARNING: goal_pad not found")
end
end
Returns true if id is non-zero and present in the current scene. Use as a guard before transform calls.
Returns the world-space position of the object's transform origin as three floats.
Moves the object to the given world-space position. Internally rebuilds the per-object BLAS and TLAS. Safe to call every frame for moving platforms and hazards.
Returns Euler rotation in degrees (YXZ order, matching the Ra Engine editor).
Sets Euler rotation in degrees (YXZ order). The primary tool for spinning hazard platforms.
-- Rotate all hazard spinners at 45°/sec on Y axis
spinner_angle = (spinner_angle + 45.0 * dt) % 360.0
for _, hid in ipairs(hazard_ids) do
engine.set_object_rotation(hid, 0.0, spinner_angle, 0.0)
end
Returns the current scale as three floats. Default is 1, 1, 1. Minimum per-axis value enforced by the engine: 0.01. Use the current scale as the basis for lerp animations to avoid drift.
-- Ease goal pad scale toward 1.0 on reveal (pop-in)
local sx, _, _ = engine.get_object_scale(goal_id)
if sx < 0.99 then
engine.set_object_scale(goal_id, sx + (1.0 - sx) * 0.15)
end
Sets object scale. Pass one value for uniform scale; pass three for non-uniform. Minimum component: 0.01.
-- Goal pad throbs between 1.0 and 1.15 on the finish screen
local sc = 1.0 + 0.15 * (math.sin(goal_scale_t * 0.8 * 2.0 * math.pi) + 1.0) * 0.5
engine.set_object_scale(goal_id, sc)
-- Checkpoint scale pop: arc up to 1.3 and back over 0.5s
local progress = 1.0 - (timer / 0.5)
local pop_sc = 1.0 + 0.3 * math.sin(progress * math.pi)
engine.set_object_scale(glow_id, pop_sc)
-- Non-uniform: wide flat platform
engine.set_object_scale(platform_id, 3.0, 0.3, 3.0)
Visual
Sets the emissive colour and intensity at runtime. Drives RTX GI — nearby geometry is lit by the emissive surface. intensity defaults to 1.0.
-- Checkpoint orange flash (4Hz, fading out over 1.2s)
local p = (math.sin(t * 4.0 * 2.0 * math.pi) + 1.0) * 0.5
engine.set_object_emissive(glow_id, 1.0, 0.5, 0.0, p * 6.0)
-- Goal pad glow (slow 1.5Hz cyan pulse)
engine.set_object_emissive(goal_glow_id, 0.0, 1.0, 0.9, 2.0 + p * 3.0)
-- Turn off
engine.set_object_emissive(glow_id, 0.0, 0.0, 0.0, 0.0)
Shows or hides an object in the ray-traced scene. Hidden objects are collapsed to a degenerate position in the BVH. Use for goal pad reveals, collectibles, and cinematic triggers. Combine with set_object_scale(id, 0.1) before revealing for a pop-in effect.
-- Reveal goal pad with a pop-in: start tiny, lerp to full size
engine.set_object_visible(goal_id, true)
engine.set_object_scale(goal_id, 0.1) -- init() resets to 1.0 on reload
Collision & Triggers
Returns six floats describing the world-space AABB of the object. Returns a degenerate zero-size AABB if the ID is invalid.
Returns the player's world-space AABB (a capsule approximation centred on camera position).
Convenience AABB overlap test between the player and a scene object. The primary method for checkpoint triggers, goal detection, and collectibles.
if goal_id and engine.player_overlaps_object(goal_id) then
state = "finished"
goal_scale_t = 0.0
engine.print_status("★ LEVEL COMPLETE!")
end
09. Scene Queries
Returns a 1-indexed Lua table of all object IDs whose GLTF node name starts with prefix. Returns an empty table if none match. The preferred way to manage groups of similarly-named objects (hazard spinners, collectibles, checkpoints).
function init()
-- Discover all hazard spinners: platform_hazard_01, _02, _03 ...
hazard_ids = engine.find_objects_by_prefix("platform_hazard_")
-- Reset each one to neutral scale
for _, hid in ipairs(hazard_ids) do
engine.set_object_scale(hid, 1.0)
end
engine.print_status("Hazards: " .. #hazard_ids)
end
Returns a 1-indexed Lua table of every GLTF node name in the current scene. Useful for debugging scene layout in init().
local names = engine.get_all_object_names()
engine.print_status(
"Level " .. current_level .. " | Objects: " .. #names ..
" | Hazards: " .. #hazard_ids
)
10. Scene Loading
Requests a deferred scene transition. The new GLTF loads at the start of the next frame — the current update() finishes cleanly first. After load, the engine calls init() again so the script re-discovers objects in the new scene.
"scenes/launch_up_level2.gltf".local LEVELS = {
"scenes/launch_up_tutorial1.gltf",
"scenes/launch_up_level1.gltf",
"scenes/launch_up_level2.gltf",
"scenes/launch_up_level3.gltf",
"scenes/launch_up_level4.gltf",
"scenes/launch_up_level5.gltf",
"scenes/launch_up_level6.gltf",
"scenes/launch_up_level7.gltf",
}
-- On level complete (KEY_N handler):
local next = current_level + 1
if next <= #LEVELS then
engine.set_var("launch_up.level", next)
engine.set_var("launch_up.peak_height", spawn_pos.y)
engine.load_scene(LEVELS[next])
-- init() is called automatically after the scene loads
end
engine.set_var() before the call are available when init() runs in the new scene.
11. Checkpoints
The engine stores one world-space respawn position. Update it as the player passes height tier thresholds.
Sets the respawn position. Call when the player reaches a new height tier or crosses a trigger zone. Typically also triggers a flash animation on the checkpoint glow object.
Returns the current respawn position. Use in the dead-state handler alongside set_camera_position().
local function do_respawn()
local cx, cy, cz = engine.get_checkpoint()
engine.set_camera_position(cx, cy, cz)
engine.set_vertical_velocity(0.0)
state = "exploring"
end
12. Game Variables
A float key-value store that lives in C++ and survives hot-reloads, scene transitions, and shutdown() / restart cycles within a session.
Stores a float value under an arbitrary string key.
Retrieves a stored value. Returns default if the key has never been set. Always provide a sensible default — the key won't exist on first run.
13. HUD & Status
Prints a line to the engine's terminal status area. Transient — overwritten on the next call. Use for event notifications (checkpoint reached, level complete, debug info). Also captured by the F1 Console View.
Pushes score and height to the in-viewport HUD overlay (top-left bitmap font readout). Call every frame while playing.
local height_gained = peak_height - spawn_pos.y
engine.set_hud_data(score, height_gained)
14. Key Constants
These globals are automatically defined in every Lua script. They map to GLFW key codes and are used with engine.get_input().
key_down and key_just_pressed tables are indexed by raw GLFW key code integers. Any GLFW_KEY_* value works — see the GLFW documentation for the full list.
15. Full Example — launch_up.lua Patterns
Annotated excerpts from the real scripts/launch_up.lua showing the most important patterns.
init() — bulk object discovery + scale reset
function init()
local _, y, _ = engine.get_camera_position()
spawn_pos.y = y
-- Restore persisted cross-session state
score = engine.get_var("launch_up.score", 0.0)
peak_height = engine.get_var("launch_up.peak_height", spawn_pos.y)
current_level = math.floor(engine.get_var("launch_up.level", 1.0) + 0.5)
-- Re-discover named objects (IDs change on every scene load)
goal_id = engine.find_object("goal_pad")
goal_glow_id = engine.find_object("goal_pad_glow")
-- Bulk-discover all hazard spinners by naming convention
hazard_ids = engine.find_objects_by_prefix("platform_hazard_")
-- Reset scale state so hot-reload doesn't leave objects at wrong size
goal_scale_t = 0.0
cp_scale_timers = {}
if goal_id then
engine.set_object_visible(goal_id, false)
engine.set_object_scale(goal_id, 1.0)
end
for _, hid in ipairs(hazard_ids) do
engine.set_object_scale(hid, 1.0)
end
engine.print_status(
"Level " .. current_level .. "/" .. #LEVELS ..
" | Hazards: " .. #hazard_ids ..
" | Score: " .. string.format("%.0f", score)
)
end
update() — hazard rotation + breathe scale
local SPINNER_SPEED = 45.0 -- degrees/second
local function update_spinner(dt)
if #hazard_ids == 0 then return end
spinner_angle = (spinner_angle + SPINNER_SPEED * dt) % 360.0
local t = engine.get_time()
local breathe = 1.0 + 0.08 * math.sin(t * 2.5)
for _, hid in ipairs(hazard_ids) do
engine.set_object_rotation(hid, 0.0, spinner_angle, 0.0)
engine.set_object_scale(hid, breathe)
end
end
Goal pad — pop-in reveal + finish throb
-- On reveal: make it visible at tiny scale, lerp to 1.0 each frame
engine.set_object_visible(goal_id, true)
engine.set_object_scale(goal_id, 0.1)
-- Each frame while revealed:
local sx, _, _ = engine.get_object_scale(goal_id)
if sx < 0.99 then
engine.set_object_scale(goal_id, sx + (1.0 - sx) * 0.15)
end
-- On finish screen: throb between 1.0 and 1.15
local sc = 1.0 + 0.15 * (math.sin(goal_scale_t * 0.8 * 2 * math.pi) + 1.0) * 0.5
engine.set_object_scale(goal_id, sc)
Checkpoint — emissive flash + scale pop
-- On checkpoint hit:
engine.set_checkpoint(cp.pos[1], cp.pos[2], cp.pos[3])
cp_scale_timers[i] = 0.5 -- 0.5s pop
-- Each frame during flash:
local p = (math.sin(t * 4.0 * 2 * math.pi) + 1.0) * 0.5
engine.set_object_emissive(glow_id, 1.0, 0.5, 0.0, p * 6.0)
local progress = 1.0 - (timer / 0.5)
local pop_sc = 1.0 + 0.3 * math.sin(progress * math.pi)
engine.set_object_scale(glow_id, pop_sc)
Quick Reference
| Function | Returns | Description |
|---|---|---|
| engine.get_camera_position() | x, y, z | Player world position |
| engine.set_camera_position(x,y,z) | — | Teleport player |
| engine.get_camera_forward() | x, y, z | Normalised look direction |
| engine.get_camera_right() | x, y, z | Normalised right vector |
| engine.get_physics_state() | table | is_grounded, is_jumping, vertical_velocity |
| engine.set_vertical_velocity(v) | — | Jump / cancel falling |
| engine.get_input() | table | key_down, key_just_pressed, mouse_delta |
| engine.get_time() | double | Wall-clock seconds (for animations) |
| engine.get_delta_time() | float | Frame delta seconds |
| engine.find_object(name) | id | ID by GLTF node name; 0 if missing |
| engine.object_exists(id) | bool | Validate an object ID |
| engine.get_object_position(id) | x, y, z | Object world position |
| engine.set_object_position(id,x,y,z) | — | Move object (triggers BLAS rebuild) |
| engine.get_object_rotation(id) | x, y, z | Euler degrees YXZ |
| engine.set_object_rotation(id,x,y,z) | — | Set rotation |
| engine.get_object_scale(id) | sx, sy, sz | Current scale (use for lerp base) |
| engine.set_object_scale(id, s) | — | Uniform scale |
| engine.set_object_scale(id,sx,sy,sz) | — | Non-uniform scale |
| engine.set_object_emissive(id,r,g,b,i?) | — | Emissive colour + intensity (drives RTX GI) |
| engine.set_object_visible(id, bool) | — | Show / hide in BVH |
| engine.get_object_aabb(id) | 6 floats | World AABB min/max |
| engine.get_player_aabb() | 6 floats | Player capsule AABB |
| engine.player_overlaps_object(id) | bool | Trigger zone test |
| engine.find_objects_by_prefix(pfx) | table of IDs | Bulk lookup by name prefix |
| engine.get_all_object_names() | table of strings | All scene node names |
| engine.load_scene(path) | — | Deferred scene transition |
| engine.set_checkpoint(x,y,z) | — | Set respawn position |
| engine.get_checkpoint() | x, y, z | Get respawn position |
| engine.set_var(key, value) | — | Persist float (survives hot-reload) |
| engine.get_var(key, default) | float | Read persisted float |
| engine.print_status(text) | — | Terminal status line (captured by F1 console) |
| engine.set_hud_data(score, height) | — | Update in-viewport HUD |