Lua scripting

Scripts give objects behaviour. They’re written in Lua 5.4, a small, friendly language, and you edit them right inside Blender.

Basics

Callbacks

The engine calls these functions when things happen. All are optional — define only what you need.

function on_start(self) end                  -- level started (or this object was spawned)
function on_update(self, dt) end             -- every frame; dt = seconds since last frame
function on_trigger_enter(self, other) end   -- this is a Trigger and something entered it
function on_trigger_exit(self, other) end    -- ...and left it
function on_interact(self, player) end       -- player pressed E looking at this (or a child of it)
prompt = "Open"                              -- text shown next to [E]; change it any time

self is the object the script is attached to. The level script gets on_start() and on_update(dt) with no self.

Use dt for anything that moves over time: self:rotate(0, 0, 90 * dt) spins at 90°/second no matter the frame rate.

Lua in two minutes

local speed = 5                     -- a variable (local = private to this script)
local name = "Gate"                  -- strings use quotes; join them with ..
local items = { "key", "map" }       -- a list (tables start at index 1)
local stats = { hp = 3, gold = 0 }   -- a table with named fields: stats.hp

if stats.hp <= 0 then                -- comparisons: == ~= < > <= >=
  respawn()
elseif stats.hp < 2 and not game.warned then
  ui.message("Careful!")
end

for i = 1, 10 do print(i) end         -- count 1..10
for _, e in ipairs(find_all("Coin")) do e.visible = true end

local function heal(n) stats.hp = stats.hp + n end
self:rotate(0, 0, 10)                -- colon = call a method on an object

A variable that hasn’t been set is nil. x = x or 0 is a handy “default to 0”.

Entity reference

Every object you get — self, other, player, or the result of find() / spawn() — has these:

Properties

NameDescription
nameThe Blender object name. Read-only.
validFalse once the object has been destroyed. Read-only.
is_playerTrue for the player. Read-only.
positionvec3, world space. Setting it on a dynamic body or the player teleports it.
rotationvec3 of XYZ Euler degrees, world space.
scalevec3.
forward, right, upThe object’s local +Y, +X and +Z axes in world space. For the player, forward is the look direction.
visibletrue / false.
velocityvec3. Writable on dynamic bodies; read-only for the player.
propsThe object’s custom properties from Blender, e.g. self.props.health.
parentThe parent object, or nil.
:children(), :find_child("Name")The objects parented directly to it; one with that name anywhere under it (copies all share part names, so find a copy’s own parts this way).
:set_material{color=vec3(1,0,0), emission=vec3(0,1,0.3), strength=4, alpha=0.5}Tints, makes glow or fades this object and its children (their own copies of the materials, so other objects don’t change). For holograms, flickering neon, a glitching death. :reset_material() puts the originals back.
net_idMultiplayer: the number every machine knows a replicated object by (0 if it isn’t). net.object(id) finds it.
:play_animation("Run", true), :crossfade, :stop_animation, :is_playing, :animations()This object’s own actions (those moving it or its bones): characters sharing action names each play their own, so outlaw:play_animation("Shoot") doesn’t make every outlaw shoot.
auto_animateOn objects with Character Animation: whether it plays its idle, walk, run and jump actions from how it moves. Set false when it dies, then play its death. :animate_character{idle=, walk=, run=, jump=} sets the component up from a script.
emittingTurns a particle emitter on or off.
nav_speedAn AI agent’s walking speed.
vehicleCars: {speed, kmh, rpm, gear, wheels, wheels_on_ground}; speed in m/s along the car (negative reversing), gear −1 in reverse. nil for other objects. Read-only.
lock_2dDynamic bodies: "plane" keeps it in the X/Z plane turning only about Y, "fixed" never turns it, false frees it. See 2D physics.
sprite_frameSprites: the frame shown (from 1). Setting it shows that frame and stops the animation.
sprite_playingSprites: whether the animation is playing. Read-only.
sprite_flip_x, sprite_flip_ySprites: mirror the picture (e.g. a character facing left).
sprite_color, sprite_alphaSprites: tint (vec3) and opacity (0–1).
sprite_layer, sprite_orderSprites: drawing order. Higher layers, then orders, cover lower ones.
sprite_lit, sprite_pixelatedSprites: shaded by the scene’s lights; sharp pixel-art sampling.
arrived, movingAI agent: reached its destination / currently walking. Read-only.

Methods

CallDescription
:move(x, y, z)Moves by an offset.
:rotate(x, y, z)Rotates locally, in degrees.
:look_at(pos)Turns so the +Y axis faces a point.
:distance_to(entity)Distance to another object, in metres.
:impulse(x, y, z)Pushes a dynamic body.
:set_physics(on)Turns collision on or off.
:destroy()Removes the object and its children at the end of the frame.
:clone()Returns a new copy.
:set_parent(entity)Attaches to another object; nil detaches.
:play_sound(), :stop_sound()Controls the object’s Sound component.
.sound_volume, .sound_pitch, .sound_playingThe Sound component while it plays: volume, pitch (1 = as recorded, 2 = an octave up; a car’s engine loop follows its RPM this way) and whether it’s playing (read-only).
:set_shape("Smile", 0.8)Sets a shape key by name or index. Overrides animation until you pass nil.
:get_shape("Smile")Reads a shape key’s current value.
:move_to(pos)AI agent: walks there along the navmesh, avoiding other agents.
:stop()AI agent: stops walking.
:play_sprite{first=1, count=4, fps=10, loop=true}Sprites: plays frames first…first+count-1 of the sheet from the start. Leave fields out to keep the sprite’s own.
:drive(throttle, steer, brake?, handbrake?)Cars: throttle −1–1 (negative reverses), steer −1–1 (positive is right), brake and handbrake 0–1. Holds until called again. See Cars & racing.
:stop_sprite()Sprites: stops on the current frame.
:emit(n)Particle emitter: emits a burst of n particles.
:call("fn", ...)Calls fn(self, ...) in that object’s script and returns the result. This is how scripts talk to each other.

World & objects

FunctionDescription
find("Name")The object with that exact name, or nil.
find_all("Prefix")A list of every object whose name starts with Prefix ("Coin" finds Coin, Coin.001…).
spawn("Template", pos?)Copies an object (usually one marked Template) and returns the copy.
spawn_model("outlaw.glb", pos?, yaw?)Loads a model file exported from Blender (relative to the level) while the game runs: its physics, scripts, AI and animation start up, and each copy has its own skeleton. Returns an object holding it (reach its parts with :find_child). With a fourth argument true, everyone sees it in multiplayer (see Multiplayer).
raycast(origin, dir, dist?)Casts a ray. Returns {entity, point, normal, distance} or nil.
looking_at()What the player aims at within reach (what the E / pick-up prompt is for), or nil. In third person it’s the usable thing nearest to where the camera looks, since the body hides what’s right in front of the player.
use(entity)Runs that object’s on_interact as if the player pressed E on it.
find_path(from, to)A list of vec3 corners along the navmesh.
random_point(around?, radius?)A random walkable point, optionally near around.
nearest_point(pos)The closest point on the navmesh.
after(seconds, fn)Runs fn once after a delay.
set_time_scale(0.3), time_scale()Bullet time: the whole game at 0.3× speed, sounds slower and lower with it (not the music). dt / time_scale() is real time.
player_aim(true, "Revolver"), player_aim_amount()Raises the player’s right arm at the crosshair, turning the hand so that child of it (its +X is the barrel) points there; false lowers it. player_aim_amount() is how far up it is (0–1): shoot once it’s nearly 1.
player_options{respawn_key=false, carrying=false}Frees R and the mouse buttons for your game (reload, shoot, aim): R stops respawning and the player stops picking things up. fly: whether V flies (on while testing in Blender, off in packaged games). crouch=false: Ctrl doesn’t crouch.
player_crouching(), player_crouch(on)Whether the player is crouched (holding Ctrl, B on a gamepad): 1.2 m tall instead of 1.8, eyes at 1.0 m, a 2.2 m/s crouch walk, no jumping, and they stay down under anything low until there’s room. player_crouch(true) crouches them until player_crouch(false): a toggle-crouch option, or a cutscene. A Player Model with player_crouch and player_crouch_walk actions plays them.
graphics{preset="low"}, graphics_quality()Quality presets: "low" (integrated graphics), "medium", "high", "ultra". The first run picks one for the GPU; other keys in the same call adjust it. graphics_quality() is the one the settings match, or "custom".
obj:break_apart(point, force), obj.broken, on_break(self)Destruction. Tick Breakable on a mesh with a body and set its Strength (the impulse a hit needs, about mass × speed; 0: only scripts break it), Pieces and how long they last. On export the add-on cuts it into that many pieces, hidden with it. A hard enough hit, or break_apart from a script (a bullet, an explosion), swaps it for its pieces flying from the point and calls on_break. In multiplayer a Replicate breakable breaks on the host for everyone.
graphics{gi=true}Real-time global illumination: light probes over the level traced on the GPU every frame, so light bounces off walls and floors and follows moving lamps and the sun with nothing baked (surfaces with a baked lightmap keep it). On in the Ultra preset; a few ms a frame.
graphics{path_tracing=true}The reference renderer: every bounce of light path traced, clearing up while the view holds still. For checking lighting and taking reference pictures. render_stats().path_samples counts the samples.
graphics{virtual_geometry=bool, vg_error=1}Virtual geometry: meshes marked Virtual Geometry are drawn from clusters at the detail the screen can show. vg_error is the error a level of detail may show, in pixels (larger draws fewer triangles); virtual_geometry=false draws them whole, to compare.
world_streaming(){cells, loaded, loading} for a level exported with World Streaming (all 0 without). Objects in a cell exist only while it’s loaded, so find() returns nil for them when it isn’t: mark things scripts need everywhere Always Loaded. Their scripts start when the cell loads.
require("name"), plugins()require loads a Lua module: name.lua from the level’s scripts folder or a plugin. It runs once per level and returns what the module returned. plugins() lists the game’s plugins.
vr.active(), vr.head(), vr.hand("right"), vr.haptic("right", 0.5, 0.1)VR (a game started with --vr): whether a headset is showing it; the head’s {position, forward, up}; a controller’s position, forward, aim_position, aim_forward (shoot along it), trigger, grip, primary (A / X), secondary (B / Y), stick_x, stick_y; and a rumble. The controllers are objects named VR Hand Left and VR Hand Right: parent a gun to one. What the player’s body holds in its right hand moves to the right controller by itself.
random(), random(max), random(a, b)A random number: 0–1, 0–max, or a–b.
play_sound("sounds/x.wav", volume?, pos?)A one-shot sound. The path is relative to the .blend; the file is copied on export.
respawn()Sends the player back to the Player Start.
load_level("Other.glb")Switches level. Each Blender scene becomes <Scene>.glb in a packaged game.
restart(), quit()Restarts the level / closes the game.
time.now, time.deltaGame time in seconds, and this frame’s length.
gameA table shared by every script.
playerThe player entity.
print(...), log(...)Write to the engine console.

Input

input.down("jump")True while the action is held.
input.pressed("use")True only on the frame it was pressed.
local x, y = input.move()Movement from WASD or the left stick.
input.gamepad()True when a controller is connected.
input.label("grab")The button for an action on what the player is using, for hints: "Left Mouse", "RB", or in VR "Right Trigger".

Actions (follow the player’s bindings, keyboard or gamepad): forward back left right jump sprint use grab throw fly fly_up fly_down respawn pause view, and two left to games: holster (H) and switch_weapon (Q).

Raw keys: A–Z, 0–9, SPACE SHIFT CTRL ALT ENTER TAB UP DOWN LEFT RIGHT F2–F9 MOUSE1 MOUSE2.

Prefer actions over raw keys where you can — they work on gamepads and respect the player’s rebinding.

UI & camera

ui.message(text, seconds?)A large centred message.
ui.set(key, text)A HUD line in the top-right corner. ui.set(key, nil) removes it.
ui.controls{{"Accelerate", "forward"}, ...}What the pause menu’s controls list shows for your game: a label for each action it uses. Players can still rebind them. Without it the list shows every action (jump, sprint, use…), which suits the walking player.
show_player_body(on)Hides (false) or shows the player’s body in both views, shadow included: cutscenes, sitting in a vehicle, invisibility.
move_player(x, y, sprint?)Walks the player as the stick would (x right, y forward) until move_player(nil). For cutscenes.
pause_game(settings?)Opens the pause menu, as Esc does; pause_game(true) opens it on its settings and controls.
camera.set(pos, look_at?)Takes over the camera, for cutscenes.
camera.release()Gives the camera back to the player.
camera.position(), camera.forward()Where the camera is and where it’s looking.
local from, dir = camera.aim()Where the player aims from: the camera’s ray, or in VR the right controller’s. Shoot along this and a game aims with the controller in a headset.
camera.fov(deg)Sets the field of view.
camera.kick(up, right?, settle?)Recoil: turns the view up (and right) by that many degrees, then lets it drift back settle of the way (0.6) over about 0.2 s.
camera.view("third")Third person (the camera follows behind the player’s head; the body turns to face where it walks); "first" switches back; camera.view() returns which. camera.view{distance=3.5, shoulder=0.35, switch=true} sets how far behind and to the right the camera sits, and whether C may switch.
camera.ortho{size=12}Orthographic (2D) view, size metres tall at any distance. camera.ortho(false) or camera.perspective(fov?) goes back. Returns the size, or nil when perspective.
local w, h = camera.view_size()Metres the view spans across and up (at a distance, for a perspective camera). Keeps a 2D camera inside the level.

Animation

play_animation("Action", loop?, speed?)Plays a Blender action. loop defaults to false. Replaces any other action moving the same objects or bones.
play_animation("Walk", {loop=true, speed=1, fade=0.3, root_motion=true})With options: fade crossfades from the current action; root_motion moves the whole object by the root bone’s motion.
crossfade("Run", 0.3)Shorthand for a looping action with a crossfade.
stop_animation("Action", fade?)Stops an action, optionally fading out.
is_playing("Action")True while it’s playing.
animations()Every action name.

Turn off World → Autoplay Animations if you want scripts to decide when animations start. For characters, use the object’s own obj:play_animation (see the Entity reference), or give it Character Animation in the add-on and it animates itself.

Saving

save("key", value)Saves a number, string, bool, table or vec3.
load("key", default)Reads a saved value, or default.
delete_save()Erases all saved data.

Saves persist between runs in %APPDATA%\BlendForge\<Game Title>\save.json.

vec3

Positions, directions and velocities are vec3 values.

local a = vec3(1, 2, 3)
local b = a + vec3(0, 0, 1) * 2     -- + - * / work as expected
print(a.x, a.y, a.z)
a:length()        a:normalized()      a:distance(b)
a:dot(b)          a:cross(b)          a:lerp(b, 0.5)   -- halfway between

When something goes wrong

Errors appear in red at the bottom-left of the game window and in the console, with the script name and line number. The broken script stops; everything else keeps running. Fix it in Blender and press Update.

Use print() to see values in the console window behind the game, or ui.set("debug", tostring(x)) to show them on screen.

Next: copy working scripts from the Script cookbook.