From zero to hero – Lua & Roblox Luau, explained the way you'd actually talk
Lua is a lightweight, fast scripting language. Roblox uses a superset called Luau, but the core is pure Lua. This section covers everything you need to get started – no fluff, just the good stuff.
You declare variables with local – that's the right way. It keeps things scoped and avoids messing up the global space. Types are: nil, boolean, number, string, function, table, thread (and userdata, but that's for later).
local name = "Alex" -- string
local age = 25 -- number
local isAlive = true -- boolean
local nothing = nil -- nil
print(type(age)) -- "number"
Why local? Because if you don't use it, the variable becomes global and can be accessed anywhere – that's a recipe for bugs. So always use local.
Tables are everything in Lua. They're like arrays, dictionaries, and objects all rolled into one. You'll use them for lists, maps, and even classes.
-- Array (list)
local fruits = {"apple", "banana", "cherry"}
print(fruits[1]) -- apple (1‑based, not 0!)
-- Dictionary (key-value)
local person = { name = "John", age = 30 }
print(person.age) -- 30
-- Mixed
local mixed = { "hello", key = 42, [3] = true }
Pro tip: You can use any value as a key – numbers, strings, even other tables. Just remember that arrays start at 1, not 0.
Functions are first‑class citizens – you can pass them around, store them in variables, and return them from other functions.
-- Named function
function greet(name)
print("Hello, " .. name)
end
greet("Lua")
-- Anonymous function assigned to a variable
local add = function(a, b) return a + b end
print(add(3, 4)) -- 7
You can also define functions with the local function syntax – it's the same, just looks cleaner sometimes.
If you've coded before, this will look familiar. Lua uses if ... then ... elseif ... else ... end. Loops are for, while, and repeat ... until.
local score = 85
if score >= 90 then
print("A")
elseif score >= 80 then
print("B")
else
print("C or below")
end
for i = 1, 5 do
print(i) -- 1,2,3,4,5
end
for key, value in pairs(person) do
print(key, value)
end
local counter = 0
while counter < 3 do
counter = counter + 1
print(counter)
end
Notice pairs for dictionaries and ipairs for arrays. But in Luau you can skip those – we'll cover that in the Luau tab.
Metatables let you change the behavior of a table – like overloading operators, handling missing keys, etc. It's the foundation of OOP in Lua.
local t1 = { value = 10 }
local t2 = { value = 20 }
local mt = {
__add = function(a, b)
return { value = a.value + b.value }
end
}
setmetatable(t1, mt)
setmetatable(t2, mt)
local t3 = t1 + t2
print(t3.value) -- 30
You can also use __index to implement inheritance – that's how you'd create classes in pure Lua.
Lua ships with a bunch of useful libraries. Here are some common ones:
gsub (replace), find, lower, upper, sub.insert, remove, sort, concat.random, floor, ceil, sin, cos.local numbers = {3, 1, 4, 1, 5}
table.sort(numbers) -- {1,1,3,4,5}
print(string.upper("hello")) -- HELLO
print(math.random(1, 10)) -- random number
Sometimes things go wrong. You can catch errors using pcall – it runs a function and returns a success flag and the result or error message.
local success, result = pcall(function()
error("something went wrong")
end)
if not success then
print("Error caught: " .. result)
end
This is super useful for preventing your whole script from crashing.
Lua lets you split code into modules. A module is just a file that returns a table (or a function). You use require to load it.
-- mymodule.lua
local M = {}
function M.sayHi()
print("Hi from module")
end
return M
-- main.lua
local mod = require("mymodule")
mod.sayHi()
Coroutines let you run functions that can be paused and resumed – great for state machines, animations, or any step‑by‑step logic.
local co = coroutine.create(function()
for i = 1, 3 do
print(i)
coroutine.yield() -- pause
end
end)
coroutine.resume(co) -- prints 1
coroutine.resume(co) -- prints 2
In standalone Lua, you can use the io library to work with files. In Roblox, this doesn't exist – but it's good to know for other projects.
local file = io.open("data.txt", "r")
if file then
local content = file:read("*a")
print(content)
file:close()
end
The debug library lets you inspect stack traces, get line numbers, and even set breakpoints programmatically. Use it carefully – it's powerful but can be slow.
function foo()
print(debug.getinfo(1).currentline) -- line number
end
foo()
Lua has a bit32 library for bitwise operations (AND, OR, XOR, shifts). Useful for low‑level stuff like flags.
local a = 5 -- 0101
local b = 3 -- 0011
print(bit32.band(a, b)) -- 1 (0001)
Lua's pattern matching is like regex but simpler. You can use %d for digits, %a for letters, . for any char, etc.
local s = "hello 123 world"
local num = string.match(s, "%d+") -- "123"
local replaced = string.gsub(s, "%d+", "XXX")
These are your bread and butter for managing lists.
local t = {"a", "c", "b"}
table.insert(t, "d") -- at end
table.remove(t, 2) -- removes "c"
table.sort(t) -- {"a","b","d"}
print(table.concat(t, ", ")) -- a, b, d
When a function captures variables from outside its scope, it forms a closure. This is how you create private data.
function makeCounter()
local count = 0
return function()
count = count + 1
return count
end
end
local counter = makeCounter()
print(counter()) -- 1
print(counter()) -- 2
You can write your own iterators using closures. This is how pairs and ipairs work under the hood.
function myRange(start, finish)
local i = start - 1
return function()
i = i + 1
if i <= finish then return i end
end
end
for num in myRange(1, 3) do print(num) end
Use ... to accept a variable number of arguments. You can pack them into a table with {...}.
function sum(...)
local total = 0
for _, v in ipairs({...}) do total = total + v end
return total
end
print(sum(1,2,3,4)) -- 10
Lua can be extended with C functions. This is how Roblox implements many of its services internally. Not something you'll use in day‑to‑day scripting, but good to know.
Lua manages memory automatically – it uses garbage collection. You can manually trigger it with collectgarbage() but usually you don't need to.
local.playerHealth not ph.Luau is Roblox's enhanced version of Lua. It adds types, modern iteration, performance tweaks, and a bunch of new libraries. Everything here works in Roblox and will make your code cleaner and faster.
You can annotate variables, parameters, and return types. It's optional but super helpful – the editor will highlight type mismatches.
local name: string = "Luau"
local age: number = 5
function greet(person: string, times: number): string
return person .. " repeated " .. tostring(times)
end
You can iterate over any array‑like table directly – no need for ipairs or pairs.
local colors = {"red", "green", "blue"}
for color in colors do
print(color) -- red, green, blue
end
for index, color in colors do
print(index, color) -- 1 red, 2 green, 3 blue
end
string.split – splits a string into parts.table.clone – shallow copy.table.find – returns index of a value.local parts = string.split("a,b,c", ",") -- {"a","b","c"}
local t = {x=1, y=2}
local copy = table.clone(t)
local idx = table.find({"cat","dog","bird"}, "dog") -- 2
You can write functions with => – they're shorter and great for one‑liners.
local add = (a, b) => a + b
print(add(2,3)) -- 5
-- With a block body:
local multiply = (a, b) => {
local result = a * b
return result
}
Same as Lua, but you can also pass custom error objects.
You can use require("folder.module") – it's cleaner.
Bitwise operations are the same as in Lua – band, bor, bxor, lshift, rshift.
Luau adds math.round, math.sign, and math.clamp.
print(math.round(3.6)) -- 4
print(math.sign(-5)) -- -1
table.clear(t) empties a table, table.move copies elements to another table.
Great for parsing.
for word in string.gmatch("foo bar baz", "%a+") do
print(word) -- foo, bar, baz
end
The task library replaces wait(). It's more precise and doesn't get throttled.
task.spawn(function()
task.wait(1)
print("After 1 second")
end)
buffer lets you work with binary data – useful for networking or file formats.
local buf = buffer.create(4)
buffer.writeu32(buf, 0, 12345)
print(buffer.readu32(buf, 0)) -- 12345
Better stack traces and more info in debug.getinfo.
Store frequently used globals in locals – it's faster.
local print = print
print("fast")
You can create type aliases for complex types.
type PlayerData = { name: string, level: number }
local data: PlayerData = { name = "Alex", level = 10 }
Use | to allow multiple types.
function getId(id: number | string): string
return tostring(id)
end
Mark a type as optional with ?.
function getAge(player: { age: number? })
return player.age or 0
end
Use never for functions that never return (like error()).
function halt(): never
error("halt")
end
You can use type and typeof to check types at runtime.
All the string goodness from Lua, plus more.
This is the good stuff – everything you need to build cool stuff in Roblox. From services to events to optimizations – we've got you covered.
Always use game:GetService("ServiceName") to get services. It's faster and more reliable than using game.ServiceName.
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
Get the player's character – but be ready for respawns. Here's the pattern I use:
local LocalPlayer = Players.LocalPlayer
local Character = LocalPlayer.Character or LocalPlayer.CharacterAdded:Wait()
-- Or just: local Character = LocalPlayer.CharacterAdded:Wait()
Here's that aimbot we talked about – simple, modern, and works.
-- (same as before – I'll keep it compact)
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local Camera = workspace.CurrentCamera
local LocalPlayer = Players.LocalPlayer
local Character = LocalPlayer.Character or LocalPlayer.CharacterAdded:Wait()
local currentTarget = nil
local isLocked = false
UserInputService.InputBegan:Connect(function(input, processed)
if processed or input.KeyCode ~= Enum.KeyCode.E then return end
isLocked = not isLocked
if not isLocked then currentTarget = nil return end
local root = Character:FindFirstChild("HumanoidRootPart")
if not root then return end
local pos = root.Position
local closest, bestDist = nil, math.huge
for _, other in Players:GetPlayers() do
if other == LocalPlayer then continue end
local char = other.Character
if not char then continue end
local otherRoot = char:FindFirstChild("HumanoidRootPart")
local otherHead = char:FindFirstChild("Head")
if not otherRoot or not otherHead then continue end
local dist = (otherRoot.Position - pos).Magnitude
if dist < bestDist then
bestDist = dist
closest = char
end
end
currentTarget = closest
end)
RunService.RenderStepped:Connect(function()
if not isLocked or not currentTarget then return end
local head = currentTarget:FindFirstChild("Head")
if head then
Camera.CFrame = CFrame.lookAt(Camera.CFrame.Position, head.Position)
end
end)
Raycasting is essential for guns, line‑of‑sight, and picking things up.
local function raycast(origin, direction, range)
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Blacklist
params.FilterDescendantsInstances = {Character}
local result = workspace:Raycast(origin, direction * range, params)
return result
end
For client‑server communication – use RemoteEvents for one‑way, RemoteFunctions for request‑response.
-- Client
local remote = game:GetService("ReplicatedStorage").MyEvent
remote:FireServer("hello")
-- Server
remote.OnServerEvent:Connect(function(player, msg)
print(player.Name .. " said: " .. msg)
end)
Use this on the server to save data before the game closes.
game:BindToClose(function()
-- save stuff
print("Server shutting down")
end)
Use DataStoreService to save player data across sessions.
local DataStoreService = game:GetService("DataStoreService")
local store = DataStoreService:GetDataStore("PlayerData")
store:SetAsync(player.UserId, {coins = 100})
PathfindingService is your friend for NPC navigation.
local PathfindingService = game:GetService("PathfindingService")
local path = PathfindingService:CreatePath()
path:ComputeAsync(startPos, endPos)
Animate properties smoothly – position, size, color, etc.
local TweenService = game:GetService("TweenService")
local tween = TweenService:Create(part, TweenInfo.new(1), {Position = Vector3.new(10,0,0)})
tween:Play()
Tag instances and retrieve them easily – great for grouping enemies or interactables.
local CollectionService = game:GetService("CollectionService")
CollectionService:AddTag(part, "Enemy")
local enemies = CollectionService:GetTagged("Enemy")
Handles all input – we used it in the aimbot. Also supports mouse movement, key up/down, etc.
Bind actions to keys with context – useful for menus or interaction prompts.
local ContextActionService = game:GetService("ContextActionService")
ContextActionService:BindAction("Jump", function(action, state, input)
if state == Enum.UserInputState.Begin then
-- jump
end
end, false, Enum.KeyCode.Space)
Manage audio globally – play sounds, adjust volume, etc.
Control which objects can collide – useful for filtering raycasts or making parts ignore each other.
Send and receive data from web APIs – but be careful, it's limited on the client.
You can set a global error handler with ScriptContext.Error to catch script errors.
For internationalization – translate strings based on player's locale.
Handle developer products, game passes, and purchases.
workspace).FindFirstChild in tight loops – use WaitForChild if you know it exists.task.wait() instead of wait() – it's more precise.Instance.new instead of cloning when possible.task.wait() – it's the new standard.