Getting Started With Lua¶
A Lua script is a text file with the .lua extension. The game loads it from a mod pack when an animation, entity, or pack-level update hook references it.
Minimal Script¶
A script returns its update function. The game runs the file once, then calls that function on every update; the function's return value tells the engine what to do next. See Script Structure.
return function()
local character = game.this_character
-- The same script can be accidentally attached to an item or global hook.
-- Guarding the context keeps the script safe.
if character == nil then
return true
end
-- This field is set by the combat controller after hitbox collision checks.
-- If the original controller has not run yet this frame, it may still reflect
-- the previous frame, so use it for reactions, not for guaranteed startup.
if character.is_hitting_someone then
character.play_sound("My Pack", "impact-heavy", 1)
end
-- true means "continue with animation behaviour or original controller".
return true
end
Add The Script To A Pack¶
- Create the script:
Editor/<Pack Name>/Scripts/Impact Sound.lua
- Open the entity in the Character Editor.
- Select the animation that should run the script.
- Assign the script in the animation logic.
- Test the animation in game.
The saved logic.json entry looks like:
{
"script": {
"packName": "My Pack",
"scriptName": "Impact Sound"
}
}
The game Global¶
The game object is the entry point for the current update. Read the context objects from it
inside the update function, on every call:
local character = game.this_character
local player = game.this_player
local enemy = game.this_enemy
local item = game.this_item
Only some of these are valid in a given context:
| Context | Valid objects |
|---|---|
| Player animation script | this_player, this_character |
| Enemy animation script | this_enemy, this_character |
| Item animation script | this_item |
| Pack-level player hook | this_player, this_character |
| Pack-level enemy hook | this_enemy, this_character |
| Pack-level item hook | this_item |
Always check context-specific objects for nil before using them.
First Safe Reads¶
Start with scripts that read state and call one helper:
return function()
local player = game.this_player
if player == nil then
return true
end
-- pressed_button_b is true only on the frame the attack button was pressed.
if player.pressed_button_b then
player.play_sound("My Pack", "debug-click", 0)
end
return true
end
First Safe Writes¶
Then move to state changes. Frame 0 is useful for one-time setup because the animation frame is advanced later by the animation controller.
return function()
local character = game.this_character
if character == nil then
return true
end
if character.current_animation_frame == 0 then
-- Stop inherited horizontal motion at the start of this animation.
character.velocity_x = 0
character.impulse_x = 0
end
return true
end
Replacing The Controller¶
If your script performs its own physics, collisions, and hit checks, return false.
return function()
local item = game.this_item
if item == nil then
return true
end
-- Item projectiles normally need manual movement when Lua owns the frame.
local should_destroy = item.check_projectile_hits()
item.velocity_x = item.velocity_x + item.impulse_x
item.velocity_y = item.velocity_y + item.impulse_z
item.inner_timer = item.inner_timer - 1
if item.inner_timer <= 0 then
should_destroy = true
end
if item.update_position_and_check_world_collisions() then
item.play_sound("Classic", "SE10", 0)
should_destroy = true
end
if should_destroy then
item.set_animation(48)
item.is_active = false
end
-- false stops fallback item physics so the projectile is not moved twice.
return false
end
Debugging¶
Use print() for temporary logs: the text goes to the game log (Player.log, or the Unity console in the editor) with a [Lua] prefix, and while debug is enabled (as when testing a pack from the editor) it is also shown on screen, at most one message per second, so a print every frame does not flood the screen. Script errors are reported through the game's error display and log, with the script name and line; this includes runtime errors in background visual scripts and HUD scripts, which stop running after their first error. If a script appears to do nothing:
- check the filename and
scriptName - check that
scriptNameomits.lua - check the pack name exactly
- confirm the animation actually reaches the scripted frame
- confirm the entity is the expected type
- guard against
nilcontext objects - return the update function from the script, and an explicit boolean from that function
- avoid
math.*,os.*, andio.*, which are not registered in the gameplay sandbox - look for a "script exceeded instruction budget" error: the script ran a loop that does not end (see below)
Lua Pitfalls¶
No math module¶
The game's Lua uses 32-bit integers and ships no math module in any script VM: gameplay, background visual (visualScript) and HUD scripts alike. math is nil, so math.floor(x) fails with "attempt to index a nil value". Division already truncates toward zero, so a / b is the integer result; write small helpers for the rest:
local function abs(x) if x < 0 then return -x end return x end
local function min(a, b) if a < b then return a end return b end
local function max(a, b) if a > b then return a end return b end
For random numbers in gameplay scripts use game.random(range), which is replay and netplay safe.
Every call has an instruction budget¶
Each call into a script (its update function, a hook, a HUD or background visual script) may run at most 10 million Lua instructions, counting everything it calls. That is far more than a normal update needs (shipped scripts use tens of thousands), so only a loop that never ends reaches it: while true do end, a repeat whose condition never becomes true, or a for loop whose bounds come from a broken value. The call then stops with a "script exceeded instruction budget" error, shown on screen with the script line. The game stays responsive, but whatever the script changed before it was stopped stays changed.
A gameplay script that exceeds the budget is disabled for the rest of the run, so its entity or stage carries on without it; a HUD or background visual script stops, as after any other error. Fix the loop and start a new run. Waiting for something across frames belongs in entity or game fields checked once per update, never in a loop inside one update.
Initialise every local¶
A local declared without a value, especially several at once (local a, b), has been seen to keep a value from an earlier pass of the same loop or update in this Lua build, instead of starting as nil. Code that then tests only one of them reads a stale value. Always give locals an explicit value, and check every value a branch uses:
-- risky: j and p may not start as nil
local j, p
-- safe
local j, p = nil, nil
if k ~= nil then j, p = find_slot(k) end
if j ~= nil and p ~= nil then
-- use j and p
end
Loading other files¶
require("Pack/module") loads Scripts/module.lua from that pack (a name without a pack prefix uses the script's own pack). dofile and loadfile take a "Pack/path/file.lua" path. Paths must stay inside the pack: .., absolute paths, drive letters and \ separators are refused, and only .lua files can be loaded.
See Troubleshooting.