Skip to content

Organizing Lua code

KILLSCRIPT starts scripts/main.lua as the client entry point. A Reflex module also starts scripts/server.lua separately. Structure each file around local state, small functions, and one initialization block.

local State = {
enabled = true,
frames = 0
}
local function Update()
State.frames = State.frames + 1
if not State.enabled then
return
end
ImGui:DrawDebugText(
"Frames: " .. tostring(State.frames)
)
end
local function Initialize()
print("Module initialized")
Scheduler:OnFrame(Update)
end
Initialize()

This order stays easy to scan:

  1. state;
  2. helpers;
  3. handlers;
  4. initialization at the end.

When main.lua becomes too large, split the client side across several Lua files. Every file must be placed directly under scripts/:

MyModule/
└── scripts/
├── main.lua # composition root and lifecycle
├── domain.lua # rules and calculations
├── game_gateway.lua # game API access
└── hud.lua # presentation

An additional file returns its public table:

scripts/domain.lua
local Domain = {}
function Domain.ClampHealth(value)
return math.max(0, math.min(value, 100))
end
return Domain

Load it from main.lua by its name, without .lua or the scripts/ prefix:

scripts/main.lua
local Domain = require("domain")
print(Domain.ClampHealth(125))

This supports a layered architecture without turning main.lua into one large file. A small module can stay in one file; extract a layer when it gains a distinct responsibility.

Additional files belong to the client side only. The Reflex server receives a single scripts/server.lua. Keep server layers as local tables and functions in that file, or bundle several source files into one server.lua before packaging.

Use local unless a value intentionally needs to be global:

local PanelVisible = true
local function TogglePanel()
PanelVisible = not PanelVisible
end

This prevents accidental name collisions and makes dependencies explicit.

Many methods, including methods on Agents, legitimately return nil while an object is unavailable:

local localAgent = Agents:GetLocalAgent()
if localAgent == nil then
return
end

Check immediately after retrieval, especially during map loading, death, spectating, and round transitions.

Scheduler:OnFrame() follows the frame rate. Load textures, sounds, and UXML once during initialization; update only necessary values each frame.

local Icon = Textures:GetTexture("Icon.png")
Scheduler:OnFrame(function()
if Icon ~= nil then
ImGui:DrawTexture(Icon, Rect.new(20, 20, 64, 64))
end
end)

Saving a folder module creates a new Lua instance. Local variables do not survive. Keep persistent user data in Storage and settings in Config.

Keep references to objects that support explicit removal:

local Line = WorldVisuals:CreateLineRenderer()
WorldVisuals:RemoveObject(Line)
Line = nil

Do not duplicate the same update code across both Reflex entry points:

  • main.lua: input, UI, local effects, and user data;
  • server.lua: server decisions and Scheduler:OnTick();
  • Network: only required messages between them.

See the Reflex architecture guide for the complete model.