Learn to code Roblox scripts with Luau
LuauJit is a free, hands-on course for Luau, the language that powers every Roblox game. Go from your first print() to events, RemoteEvents, real game systems, and anti-tamper. Stuck? The AI tutor on every page answers in code.
Write real scripts
Every lesson is real Luau you can paste straight into Roblox Studio and run.
Learn the hard parts
Client vs server, RemoteEvents, and sanity checks, the things most tutorials skip.
Protect your game
A full lesson on anti-tamper: server authority, validation, rate limits, and secrets.
1. Getting Started
Luau is Roblox's own version of Lua 5.1: faster, safer, and built for games. Everything in a Roblox world, from a coin to the camera, is controlled by scripts written in Luau.
Set up in 2 minutes
- Install Roblox Studio and open it.
- Create a new place from the Baseplate template.
- In the Explorer panel, hover over ServerScriptService, click the +, and choose Script.
A script opens with print("Hello world!") already in it. Press the big Play button and open the Output window (View > Output). You'll see it printed.
Your first script
-- a script in ServerScriptService
print("Hello from LuauJit!")
-- print can take many values at once
print("Score:", 100, true)2. Variables & Types
A variable is a name that holds a value. In Luau you create one with local, which keeps it inside the current script scope.
local playerName = "Kers0ne" -- string local coins = 250 -- number local isVIP = true -- boolean (true/false) local target = nil -- nothing yet local speed = 16 * 2 -- math works here
Strings
local greeting = "Hello"
local name = "world"
-- two ways to join text
print(greeting .. ", " .. name) -- old style, two dots
print(`{greeting}, {name}!`) -- Luau string interpolationThe backtick style is Luau-only and much cleaner. Anything inside { } is dropped in as a value.
Numbers and math
local base = 10 base += 5 -- 15, Luau has += -= *= /= too print(base ^ 2) -- 225 (exponent) print(7 % 3) -- 1 (remainder)
local. A variable without it becomes global, which is slower and easy to overwrite by accident.3. Tables & Loops
Tables are the one data structure Luau has, and you will use them for everything: lists, inventories, settings, databases.
Arrays (ordered lists)
local fruits = {"apple", "banana", "cherry"}
print(fruits[1]) -- "apple" (tables start at 1, not 0!)
print(#fruits) -- 3 (the # operator is the length)
fruits[2] = "blueberry" -- replace one
table.insert(fruits, "date") -- add to the end
table.remove(fruits, 1) -- remove index 1Dictionaries (name > value)
local stats = {
coins = 250,
level = 3,
vip = true,
}
print(stats.coins) -- 250
stats.coins += 50 -- 300Loops
local fruits = {"apple", "banana", "cherry"}
-- loop a fixed number of times
for i = 1, 5 do
print("lap " .. i)
end
-- walk an array
for index, fruit in ipairs(fruits) do
print(index, fruit)
end
-- walk a dictionary
for key, value in pairs(stats) do
print(key, value)
end
-- repeat while a condition holds
local hp = 100
while hp > 0 do
hp -= 25
endipairs for arrays, pairs for dictionaries. Roblox scripts run once top to bottom, so anything that must keep running (a countdown, a spawner) lives inside a loop.4. Functions & Modules
Functions are reusable blocks of logic. Modules are scripts that share code between your other scripts, so you write a thing once and use it everywhere.
Functions
-- define once
local function giveCoins(player, amount)
print(player.Name .. " got " .. amount .. " coins")
return amount * 2 -- functions can return values
end
-- call many times
local doubled = giveCoins("Alice", 50) -- prints, returns 100Luau functions can return more than one value, which is great for "result + reason" patterns:
local function tryPurchase(player, price)
if player.coins < price then
return false, "not enough coins"
end
player.coins -= price
return true, "ok"
end
local ok, reason = tryPurchase(somePlayer, 100)ModuleScripts
Create a ModuleScript in ReplicatedStorage. It must return exactly one table:
-- ModuleScript named "Rewards" in ReplicatedStorage
local Rewards = {}
function Rewards.dailyBonus(player)
return 100
end
return Rewards-- any Script that needs it local Rewards = require(game.ReplicatedStorage.Rewards) local bonus = Rewards.dailyBonus(player)
5. Instances & Workspace
Everything you see in a Roblox world is an Instance: Parts, Scripts, Players, GUIs. Scripts create, move, and delete them in real time.
Create a part from code
local part = Instance.new("Part")
part.Name = "TreasureChest"
part.Size = Vector3.new(4, 2, 4)
part.Position = Vector3.new(0, 5, -10)
part.Anchored = true
part.BrickColor = BrickColor.new("Gold")
part.Parent = workspaceFind and change things
-- two ways to reach an instance
local spawn1 = workspace:FindFirstChild("SpawnLocation")
local spawn2 = workspace.SpawnLocation -- errors if missing
-- walk every child
for _, child in ipairs(workspace:GetChildren()) do
if child:IsA("BasePart") then
child.Transparency = 0.5
end
end
-- copy values between instances
part2.CFrame = part1.CFramepart.Anchored = true unless you want physics to move it.6. Events
Scripts don't just run once. Events are the heartbeat of Roblox: your code waits, and when something happens (a touch, a player joining, a click), your function fires.
Connect an event
local part = workspace.TreasureChest
part.Touched:Connect(function(hit)
local character = hit.Parent
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
print(humanoid.Parent.Name .. " touched the chest")
end
end)Player lifecycle
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(player)
print(player.Name .. " joined!")
player.CharacterAdded:Connect(function(character)
print(player.Name .. " spawned")
end)
end)
Players.PlayerRemoving:Connect(function(player)
print(player.Name .. " left")
end):Connect() fires every time, :Once() fires one time and disconnects itself. Use Once for things like a one-time tutorial trigger.7. Client vs Server
This is the lesson that separates script kiddies from real developers. Roblox has two worlds running at once:
- Server (Scripts in ServerScriptService): the truth. Runs once for everyone, sees everything, and is much harder to cheat against.
- Client (LocalScripts, usually in StarterPlayerScripts or StarterGui): one player's view and input. A player can replace, read, or stop any client script, so client code must never be trusted.
RemoteEvents connect the two
A RemoteEvent object (create it in ReplicatedStorage) is a bridge: the client asks, the server decides.
-- CLIENT (LocalScript): fire the request
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remote = ReplicatedStorage:WaitForChild("BuyItem")
remote:FireServer("sword")-- SERVER (Script): validate, then act
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remote = ReplicatedStorage:WaitForChild("BuyItem")
local PRICES = { sword = 500, shield = 350 }
remote.OnServerEvent:Connect(function(player, itemName)
-- 1. validate the argument type
if type(itemName) ~= "string" then return end
-- 2. validate the value
local price = PRICES[itemName]
if not price then return end
-- 3. only NOW act
-- (check the player really has the coins here)
print(player.Name .. " bought " .. itemName)
end)8. Build Something Real
Let's put it together: a coin leaderboard and a working shop purchase. These two systems appear in almost every Roblox game.
Leaderstats (the leaderboard)
-- Script in ServerScriptService
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(player)
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local coins = Instance.new("IntValue")
coins.Name = "Coins"
coins.Value = 0
coins.Parent = leaderstats
end)That's it. Roblox reads any leaderstats folder on the player and shows it on the player list automatically.
A working shop
-- Script in ServerScriptService, plus a RemoteEvent named "BuyItem"
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remote = ReplicatedStorage:WaitForChild("BuyItem")
local ITEMS = {
SpeedBoost = { price = 100, speed = 24 },
JumpBoost = { price = 150, jump = 75 },
}
remote.OnServerEvent:Connect(function(player, itemName)
local item = ITEMS[itemName] -- nil for unknown names
if not item then return end
local coins = player.leaderstats.Coins
if coins.Value < item.price then return end
coins.Value -= item.price -- charge on the server
local character = player.Character
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
if humanoid and item.speed then
humanoid.WalkSpeed = item.speed
end
if humanoid and item.jump then
humanoid.UseJumpPower = true
humanoid.JumpPower = item.jump
end
end)9. Anti-Tamper & Security
Cheaters attack the client, because that's the only thing they control. Your defense is not clever client tricks; it is a server that assumes every client is hostile.
Rule 1: The server is the truth
Never let a client tell the server something important. Wrong:
-- BAD: client says how much damage it did
remote.OnServerEvent:Connect(function(player, damage)
enemy.Humanoid:TakeDamage(damage) -- cheater sends 999999
end)-- GOOD: server computes damage from its own data
remote.OnServerEvent:Connect(function(player, targetId)
local weapon = getEquippedWeapon(player) -- server's own record
if not weapon then return end
local target = findTarget(targetId)
if not target then return end
target.Humanoid:TakeDamage(weapon.damage) -- server's number
end)Rule 2: Validate every remote
remote.OnServerEvent:Connect(function(player, itemName)
-- type check: exploiters can send tables, NaN, anything
if type(itemName) ~= "string" then return end
if #itemName > 30 then return end -- sane length
if not ITEMS[itemName] then return end -- known item only
-- ... then business logic
end)Rule 3: Rate limit (cooldowns)
local lastFire = {}
remote.OnServerEvent:Connect(function(player, itemName)
local now = os.clock()
if lastFire[player] and now - lastFire[player] < 1 then
return -- too fast, drop it
end
lastFire[player] = now
Players.PlayerRemoving:Connect(function(leaving)
lastFire[leaving] = nil -- clean up
end)
-- ... business logic
end)Rule 4: Keep secrets on the server
Anything in a Script, ServerStorage, or ServerScriptService is invisible to players. Anything in ReplicatedStorage, StarterGui, or a LocalScript can be read and copied. API keys, admin lists, and formulas live server-side only.
-- server-side admin check, never a client-side one
local ADMIN_IDS = {
[12345678] = true,
}
script.Parent.AdminAction.OnServerEvent:Connect(function(player, action)
if not ADMIN_IDS[player.UserId] then return end
-- only real admins reach here
end)Rule 5: Sanity-check the physics
-- server watches for impossible movement
local MAX_SPEED = 50
spawn(function()
while task.wait(5) do
for _, player in ipairs(Players:GetPlayers()) do
local char = player.Character
local root = char and char:FindFirstChild("HumanoidRootPart")
if root then
-- server compares position jumps over time
-- if a player teleports far more than max speed allows,
-- flag or teleport them back
end
end
end
end)10. Level Up Luau
Luau has real type checking, modern async tools, and OOP patterns. These make your code faster to write and harder to break.
Type annotations
local function addCoins(player: Player, amount: number): number
local coins = player.leaderstats.Coins
coins.Value += amount
return coins.Value
end
type Item = {
name: string,
price: number,
}
local sword: Item = { name = "sword", price = 500 }Studio checks these as you type and flags mistakes before you ever press Play. Turn on strict mode in Script Properties for the full benefit.
The task library
-- never use wait() or spawn() anymore
task.wait(2) -- exact-ish timing
task.spawn(function() -- run alongside current code
while true do
task.wait(10)
-- periodic work
end
end)
task.delay(5, function() -- run after 5 seconds
print("5 seconds later")
end)OOP with metatables
local Enemy = {}
Enemy.__index = Enemy
function Enemy.new(name: string, hp: number)
return setmetatable({ name = name, hp = hp }, Enemy)
end
function Enemy:takeDamage(amount: number)
self.hp -= amount
if self.hp <= 0 then
print(self.name .. " is defeated")
end
end
local spider = Enemy.new("Spider", 40)
spider:takeDamage(30)Where to go next
- DataStores: save player data between sessions.
- Raycasting: lasers, line of sight, guns.
- CollectionService tags: give one script power over many objects.
- ProfileService / replica libraries: community standards for safe data.
- The Roblox Creator docs and the Luau language site are the two references worth bookmarking.