Jit

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.

10 lessons Beginner to security AI tutor built in

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

  1. Install Roblox Studio and open it.
  2. Create a new place from the Baseplate template.
  3. 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)
Try it: change the text, hit Play again, and check the Output. Output is where scripts talk to you, so learn to love it early.
Ask the tutor about this lesson

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 interpolation

The 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)
Rule of thumb: always use local. A variable without it becomes global, which is slower and easy to overwrite by accident.
Ask the tutor about this lesson

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 1

Dictionaries (name > value)

local stats = {
    coins = 250,
    level = 3,
    vip = true,
}

print(stats.coins)          -- 250
stats.coins += 50           -- 300

Loops

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
end
Remember: ipairs 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.
Ask the tutor about this lesson

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 100

Luau 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)
Why bother? Without modules, fixing one bug means hunting it down in ten scripts. With modules, you fix it once.
Ask the tutor about this lesson

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 = workspace

Find 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.CFrame
Anchored matters: an unanchored part falls with gravity. Set part.Anchored = true unless you want physics to move it.
Ask the tutor about this lesson

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 vs Once: :Connect() fires every time, :Once() fires one time and disconnects itself. Use Once for things like a one-time tutorial trigger.
Ask the tutor about this lesson

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)
Golden rule: the client can send ANY arguments, including fake ones, as many times per second as it wants. The server checks everything: type, value, money, cooldowns. Lesson 9 goes deep on this.
Ask the tutor about this lesson

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)
Notice the pattern: validate the name, check the balance, charge, then apply the effect, all on the server. The client only said "I want SpeedBoost"; it never told the server how much it costs.
Ask the tutor about this lesson

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)
Mindset: assume the cheater has already read every client script and can send any remote call with any arguments. Design so that knowing everything changes nothing. Roblox's own Creator docs have a whole section on this; read it after this lesson.
Ask the tutor about this lesson

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.
Ask the tutor about this lesson