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.
Basic main.lua structure
Section titled “Basic main.lua structure”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:
- state;
- helpers;
- handlers;
- initialization at the end.
Split client code into files
Section titled “Split client code into files”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 # presentationAn additional file returns its public table:
local Domain = {}
function Domain.ClampHealth(value) return math.max(0, math.min(value, 100))end
return DomainLoad it from main.lua by its name, without .lua or the scripts/ prefix:
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.
Prefer local declarations
Section titled “Prefer local declarations”Use local unless a value intentionally needs to be global:
local PanelVisible = true
local function TogglePanel() PanelVisible = not PanelVisibleendThis prevents accidental name collisions and makes dependencies explicit.
Check nil at API boundaries
Section titled “Check nil at API boundaries”Many methods, including methods on Agents, legitimately return nil while an object is unavailable:
local localAgent = Agents:GetLocalAgent()
if localAgent == nil then returnendCheck immediately after retrieval, especially during map loading, death, spectating, and round transitions.
Avoid expensive work every frame
Section titled “Avoid expensive work every frame”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)) endend)Account for hot reload
Section titled “Account for hot reload”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 = nilSeparate client and server
Section titled “Separate client and server”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 andScheduler:OnTick();Network: only required messages between them.
See the Reflex architecture guide for the complete model.