Halo Lua - Common References
Table of Contents
- Loading table of contents...
This document collects pure Lua utility functions that work across SAPP, Phasor, and Chimera without relying on platform-specific APIs. They are safe to use in any Lua 5.1+ environment.
For platform-specific APIs, see the dedicated tutorials:
Compatibility Note: math.atan2
Some Lua environments may not expose math.atan2. Use this fallback to ensure, for example, that cardinal direction
functions work everywhere:
if not math.atan2 then
local pi = math.pi
math.atan2 = function(y, x)
if x > 0 then
return math.atan(y / x)
elseif x < 0 then
return (y >= 0 and math.atan(y / x) + pi or math.atan(y / x) - pi)
else
if y > 0 then return pi / 2
elseif y < 0 then return -pi / 2
else return 0 end
end
end
end
Utility Functions
Check if Two Points are Within a Radius (3D, squared distance)
Uses squared distance to avoid expensive math.sqrt - ideal for per-tick checks.
Parameters:
x1, y1, z1, x2, y2, z2 (numbers) - Coordinates of the two points.
radius (number) - Distance threshold.
Returns: true if within radius, false otherwise.
function points_in_range(x1, y1, z1, x2, y2, z2, radius)
local dx = x1 - x2
local dy = y1 - y2
local dz = z1 - z2
return (dx * dx + dy * dy + dz * dz) <= (radius * radius)
end
Convert Camera Direction to Cardinal Point (N, NE, E, …)
Converts a direction vector (e.g., from camera forward vector) into a compass point. Includes the math.atan2 fallback
shown above.
Parameters:
fx, fy (numbers) - X and Y components of the direction vector.
Returns: String like "N", "NE", "E", etc.
function direction_to_cardinal(fx, fy)
local angle = (90 - math.deg(math.atan2(fy, fx))) % 360
local dirs = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"}
local idx = math.floor((angle + 22.5) / 45) % 8 + 1
return dirs[idx]
end
Deep Copy a Table (Handles Nested Tables and Metatables)
Creates a fully independent copy of a table, including nested structures and metatables.
Warning: Does not handle circular references (will cause infinite recursion).
Parameters:
orig (any) - The value or table to copy.
Returns: A deep copy.
function deep_copy(orig)
if type(orig) ~= "table" then return orig end
local copy = {}
for k, v in pairs(orig) do
copy[deep_copy(k)] = deep_copy(v)
end
return setmetatable(copy, deep_copy(getmetatable(orig)))
end
Shuffle an Array (Fisher-Yates)
Randomly shuffles an array-style table in place. Every permutation is equally likely.
Parameters:
t (table) - Table with sequential integer keys starting at 1.
Returns: Nothing (modifies the table in place).
function shuffle_array(t)
for i = #t, 2, -1 do
local j = math.random(i)
t[i], t[j] = t[j], t[i]
end
end
Note: Call math.randomseed(os.time()) once at script load for proper randomness.
Get Number of Key-Value Pairs in Any Table
Works for both array-style and dictionary-style tables (unlike Lua’s # operator).
Parameters:
t (table) - Any table.
Returns: Number of key-value pairs.
function table_length(t)
local count = 0
for _ in pairs(t) do
count = count + 1
end
return count
end
Parse Command Arguments by Delimiter
Splits a string into substrings based on a delimiter - useful for parsing chat commands or CSV data.
Parameters:
input (string) - The string to split.
delimiter (string) - The delimiter character (e.g., " ", ",").
Returns: An array-like table of substrings.
function parse_args(input, delimiter)
local result = {}
for substring in input:gmatch("([^" .. delimiter .. "]+)") do
result[#result + 1] = substring
end
return result
end
Example:
parse_args("/give weapon sniper", " ") → {"/give", "weapon", "sniper"}
Format Messages - Three Approaches
Version 1: Classic string.format style
Define message templates as constants and use a wrapper that behaves like string.format.
local HELLO_MESSAGE = "Hello world!"
local PLAYER_JOINED = "Player %s has joined the game."
local PLAYER_SCORE = "%s scored %d points in %d minutes."
local function format_message(message, ...)
if select('#', ...) > 0 then
return message:format(...)
end
return message
end
-- Usage:
print(format_message(HELLO_MESSAGE))
print(format_message(PLAYER_JOINED, "Chalwk"))
print(format_message(PLAYER_SCORE, "Chalwk", 150, 12))
Version 2: Placeholder-based (named variables)
Use named placeholders like $name and replace from a table.
local SCORE_MESSAGE = "$name scored $points points in $minutes minutes."
local JOIN_MESSAGE = "Player $name has joined the server."
local function format_message(message, vars)
return (message:gsub("%$(%w+)", function(key)
return vars[key] or "$" .. key
end))
end
-- Usage:
print(format_message(JOIN_MESSAGE, {name = "Chalwk"}))
print(format_message(SCORE_MESSAGE, {name = "Chalwk", points = 150, minutes = 12}))
Version 3: Case-insensitive placeholders with fallback
This version matches placeholders like $NAME, $Name, or $name to a key in the args table by trying the original
key, then lowercased, then uppercased. Missing placeholders are left unchanged.
local function format(template, args)
if not args then return template end
return (template:gsub("%$([%w_]+)", function(key)
local value = args[key] or args[key:lower()] or args[key:upper()]
return value ~= nil and tostring(value) or "$" .. key
end))
end
Usage examples:
print(format("Hello $name, you have $points points!", {name = "Chalwk", points = 42}))
-- → "Hello Chalwk, you have 42 points!"
print(format("Welcome $NAME", {name = "Chalwk"}))
-- → "Welcome Chalwk" (matches because $NAME -> args["name"]:upper())
print(format("Score: $score", {}))
-- → "Score: $score"
-- No args table → returns template unchanged
print(format("Plain text"))
-- → "Plain text"
Performance Best Practices
The following tips help you write efficient Lua scripts that run smoothly with SAPP, Phasor and Chimera. They are especially important for code that executes frequently, such as per-tick callbacks.
Localize Heavily Used Globals
Cache frequently used globals into local variables at the top of your script or function. For example:
local table_insert = table.insert
local math_random = math.random
local string_sub = string.sub
Local variable access is faster than global table lookups. This small optimisation adds up in hot code paths that run many times per second.
Be GC-Aware - Control Collection During Quiet Moments
Lua’s garbage collector can cause small pauses when it runs. If you need to force collection, use collectgarbage()
tactically - for example, during round end or idle periods. Use this sparingly and measure the impact first. In most
cases, letting the collector run automatically is fine.
Minimize Garbage - Reuse Tables / Object Pools
Creating many small temporary tables each tick increases garbage collector churn and can cause frame hitches. Reuse tables with a simple pool instead of allocating new ones repeatedly.
Simple pool pattern:
local pool = {}
local function new_table()
return table.remove(pool) or {}
end
local function free_table(t)
for k in pairs(t) do t[k] = nil end
pool[#pool + 1] = t
end
Use new_table() to obtain a table and free_table(t) to return it to the pool when you are done.
Avoid Heavy Work Inside Callbacks - Batch and Defer
If an event fires often, such as OnTick or OnClientUpdate, do the minimum work inside the callback. Push heavy
processing to a timer or queue that runs at a lower frequency, for example every 200 milliseconds.
Pattern:
- The callback pushes a lightweight record (e.g., a player ID and timestamp) into a table.
- A separate timer, running every 200 ms, drains that table and performs the expensive operations.
This keeps the fast path lean and prevents frame rate drops.
Timer Functions for Delayed/Repeating Work
Each platform provides its own timer function for scheduling work after a delay. Use these to defer non-critical tasks or run periodic checks without blocking the main game loop.
- SAPP:
timer(ms, callback, ...) - Chimera:
set_timer(ms, callback, ...) - Phasor:
registertimer(ms, callback, ...)
If the callback returns true, it repeats every ms milliseconds. The following example uses SAPP syntax:
function OnPlayerJoin(player_id)
timer(5000, "PostJoinTask", player_id)
end
function PostJoinTask(player_id)
-- do something after 5 seconds
end
Adapt the function name to match your target platform.