luaguides

Getting Started with Roblox Luau

What is Luau?

If you are getting started with Roblox game development, Luau is the scripting language you will work with every day. Built into Roblox Studio, it is a descendant of Lua 5.1 designed specifically for Roblox game development. If you have used Lua before, Luau will feel familiar, but it has its own quirks worth knowing.

Unlike plain Lua, Luau adds optional static typing, better performance, and tighter integration with Roblox’s object model. Every interactive element in a Roblox game — parts, UI, tools, animations — gets its behavior from Luau scripts.

This tutorial assumes you have never written a line of code. By the end, you will have a working knowledge of Luau fundamentals and your first interactive Roblox game object.

Setting up Roblox Studio

If you do not already have Roblox Studio:

  1. Go to roblox.com/create and click “Start Creating”.
  2. Download and install Roblox Studio.
  3. Open Studio and sign in with your Roblox account.

Create a new place by clicking “New Place”. Choose the “Baseplate” template — it gives you an empty world to work in.

To add a script to an object:

  • In the Explorer panel, click on the object you want to script (e.g., Workspace).
  • Click the small + button next to the object.
  • Find “Script” under “Basic Objects” and click it.

You will see a code editor open on the right side of Studio. That is where you write Luau.

Your first script

Inside the new Script, you already see a line of code:

print("Hello world!")

This prints a message to the Output panel in Studio. Press Play (the green triangle at the top) to run your game. Look at the Output panel at the bottom of Studio. You will see Hello world! printed there.

You just ran your first Luau code.

Variables and types in Luau

Variables are named containers that hold a value in memory so you can refer to it later without repeating the same data. In Luau, every variable should be declared with the local keyword, which limits its visibility to the current block or file. Without local, a variable becomes global, and global variables can cause hard-to-find bugs when two scripts accidentally use the same name. Using local keeps your code predictable and avoids collisions across different parts of your game:

local playerName = "LuauRookie"
local health = 100
local isAlive = true

Luau has a handful of basic types:

TypeExampleNotes
string"hello"Text in quotes
number42, 3.14All numbers are doubles
booleantrue, falseLogical values
nilnilMeans “no value”

Luau is dynamically typed, so you do not need to declare the type of a variable. The engine figures out the type at runtime based on whatever value you assign. This makes Luau quick to write: you can store a number in a variable one moment and a string the next without any extra ceremony. However, Luau does support optional type annotations for when you want more safety and tooling support. Adding types helps Studio’s autocomplete work better and catches mistakes before you even run your code:

local name: string = "Roblox"
local score: number = 0

Once you have created a variable, you can change its value as many times as you need. Luau simply overwrites the old value with the new one, which is useful for counters, state tracking, and accumulating results over time. Reassignment works the same way whether or not you used a type annotation:

local count = 10
count = count + 1  -- count is now 11

Tables: Luau’s only data structure

Everything that is not a basic type in Luau is a table. Tables are the single data structure for both key-value maps and ordered arrays. You will use tables constantly.

You create a table by wrapping key-value pairs or values in curly braces. When you use string keys, the table acts like a dictionary where each key maps to a specific value. A table with string keys looks like this:

local player = {
    name = "Player1",
    health = 100,
    level = 5
}

print(player.name)          -- dot notation: prints "Player1"
print(player["health"])     -- bracket notation: prints 100

The same table structure can also store data sequentially using integer keys. When you create a table with values separated by commas and no explicit keys, Luau automatically assigns numeric indices starting from 1. This dual-purpose design means you use the same table type whether you need a dictionary, a list, or even a mix of both in a single structure:

local inventory = {"sword", "shield", "potion"}
print(inventory[1])  -- Luau arrays are 1-indexed: prints "sword"

Both are the same table type under the hood; the difference is only in how you use the keys.

Control flow: if/else and loops

Programs rarely run in a straight line from top to bottom. You often need to make decisions based on conditions or repeat a block of code multiple times. Luau gives you standard tools for this: if statements for branching and several loop constructs for repetition. These control-flow structures work the same way as in most programming languages, with minor syntax details to remember.

Conditionals

local health = 30

if health < 50 then
    print("Low health!")
elseif health < 100 then
    print("Getting hurt")
else
    print("Fully healthy")
end

elseif and else are optional. The end keyword closes every if block.

When you need to repeat an action a known number of times, a numeric for loop is the tool for the job. It counts from a starting number to an ending number, running the loop body once per step. The loop variable (commonly i) holds the current count value and updates automatically each iteration:

Numeric for loop

for i = 1, 10 do
    print("Count: " .. i)  -- ".." concatenates strings
end

Iterating tables

Numeric for loops work well when you know the exact range, but tables store data without a predictable integer sequence when they use string keys. To walk through every entry in a table regardless of its key type, Luau provides the pairs() iterator. It yields each key-value pair one at a time, making it the go-to choice for inspecting dictionary-style tables:

local scores = {
    Player1 = 100,
    Player2 = 85,
    Player3 = 92
}

for name, score in pairs(scores) do
    print(name .. " scored " .. score)
end

pairs() iterates over key-value pairs in arbitrary order. ipairs() iterates over array-style tables in sequential order, stopping at the first nil value. Note that pairs() does not guarantee any particular order; if you need sorted output, collect the keys into an array and sort them first.

A while loop keeps running as long as a condition stays true, making it useful when you do not know ahead of time how many iterations are needed. You might wait for a player to reach a target, drain a health pool, or poll for a changing game state. The condition is checked at the start of each iteration:

While loop

local count = 1

while count <= 5 do
    print(count)
    count = count + 1
end

Functions

Functions let you wrap a chunk of logic into a named block that you can call from anywhere in your script. Instead of copying the same code into multiple places, you write the logic once inside a function and invoke it whenever needed. Functions can accept input through parameters and send results back to the caller with return. This keeps your scripts organized as they grow in complexity:

local function greet(name)
    return "Hello, " .. name
end

local message = greet("Roblox")
print(message)  -- prints "Hello, Roblox"

Unlike many other languages where a function can return only one value, Luau allows a function to hand back multiple results in a single return statement. The caller then captures each returned value into a separate variable by listing them on the left side of the assignment. This pattern is common in Roblox APIs and can make your code more expressive without needing temporary tables or wrapper objects:

local function getStats()
    return 100, 50, "Player"
end

local health, mana, role = getStats()
print(health)  -- 100
print(mana)    -- 50

Understanding scripts: Script vs LocalScript vs ModuleScript

Roblox has three script types:

Script (server-side) Runs on the Roblox server. Can manipulate the world, handle game logic, and access all services. Does not have access to the player’s screen or local inputs.

LocalScript (client-side) Runs on each player’s device. Can access player-specific things like Players.LocalPlayer, the camera, and user input. Use for UI, local animations, and input handling.

ModuleScript Does not run on its own. Stores reusable code that other scripts import with require(). Use to share logic between server and client scripts.

-- In a ModuleScript called "PlayerUtils"
local module = {}

function module.getPlayerByName(name)
    return game.Players:FindFirstChild(name)
end

return module

Once a ModuleScript is saved, other scripts pull it in with the require() function. You pass require() the path to the ModuleScript object in the Explorer hierarchy. The function returns whatever the module’s return statement produced; typically a table of functions. You then call those functions through the returned table just like any other Luau table. This pattern keeps your codebase modular: put shared logic in ModuleScripts and let server and client scripts both require() the same module without duplicating code:

-- In a Script, import the module
local PlayerUtils = require(path/to/PlayerUtils)
local player = PlayerUtils.getPlayerByName("Player1")

Roblox organizes everything in a hierarchy starting from game. Two of the most important services are:

game.Players contains all currently connected players. Each player has a Player object and a Character model. The Players service lets you count online players, listen for joins and leaves, and access individual player data like leaderstats and character models. Always use game:GetService("Players") instead of game.Players directly. GetService is the recommended pattern in Roblox because it handles service creation and caching properly:

local Players = game:GetService("Players")

local playerCount = #Players:GetPlayers()
print("There are " .. playerCount .. " players online")

game.Workspace is the 3D world. Contains all Parts, Terrain, Models, and Cameras. Anything you want visible to players must be parented somewhere under Workspace. You can create new objects with Instance.new(), set properties like size and color, and then assign workspace as the parent to spawn them into the live game world. The Workspace is also where physics, lighting, and sound propagate during gameplay:

local part = Instance.new("Part")
part.Name = "MyPart"
part.Size = Vector3.new(4, 1, 2)      -- x=4, y=1, z=2 studs
part.Position = Vector3.new(0, 3, 0)    -- 3 studs above ground
part.BrickColor = BrickColor.new("Bright red")
part.Anchored = true                   -- stays in place (not affected by gravity)
part.Parent = workspace                -- add it to the world

Connecting to events with :Connect()

Everything interactive in Roblox works through events. An event signals that something happened: a player joined, a part was touched, a timer expired. You “listen” to an event by connecting a function to it with the :Connect() method. Roblox calls your function automatically each time the event fires, passing relevant data like the player who joined or the object that triggered the touch. This is the core pattern for making your game feel alive and responsive:

local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(player)
    print(player.Name .. " joined the game!")
end)

The :Connect() method registers a callback and returns a connection object that you can use later to disconnect the listener if needed. Every time the event fires, your function runs with the arguments the event provides. Events are the backbone of Roblox scripting: they let your code react to what happens in the game world rather than polling or guessing. Here is a more practical example: a Part that changes color when touched:

local part = workspace.Part

local colors = {
    Color3.fromRGB(255, 0, 0),    -- red
    Color3.fromRGB(0, 255, 0),    -- green
    Color3.fromRGB(0, 0, 255)     -- blue
}
local currentIndex = 1

part.Touched:Connect(function(hit)
    -- Change to the next color
    currentIndex = (currentIndex % #colors) + 1
    part.Color = colors[currentIndex]
end)

Some important gotchas with events:

  • Touched fires for every body that touches the Part, including other Parts, NPCs, and the player’s character. Check hit.Name or hit.Parent if you need to filter.
  • Server scripts and LocalScripts live in different environments. A Touched event on a Part in a server Script fires for all players; in a LocalScript it only fires on the local player’s device.

Your first hands-on project: a color-changing billboard

Here is a mini-project to apply what you have learned. This is a server Script (not a LocalScript), which means it runs on the server and the color change will be visible to all players.

  1. In Roblox Studio, insert a Part into Workspace.
  2. Add a Script as a child of that Part.
  3. Paste this code:
local part = script.Parent

-- Create a BillboardGui on the part
local billboard = Instance.new("BillboardGui")
billboard.Size = UDim2.new(4, 0, 1.5, 0)
billboard.StudsOffset = Vector3.new(0, 3, 0)
billboard.AlwaysOnTop = true
billboard.Parent = part

local label = Instance.new("TextLabel")
label.Size = UDim2.new(1, 0, 1, 0)
label.BackgroundTransparency = 0.5
label.BackgroundColor3 = Color3.new(0, 0, 0)
label.TextColor3 = Color3.new(1, 1, 1)
label.TextScaled = true
label.Font = Enum.Font.GothamBold
label.Text = "Touch me!"
label.Parent = billboard

-- Track touch count and cycle colors
local touchCount = 0
local colors = {
    Color3.fromRGB(200, 50, 50),
    Color3.fromRGB(50, 200, 50),
    Color3.fromRGB(50, 50, 200),
    Color3.fromRGB(200, 200, 50)
}

part.Touched:Connect(function(hit)
    touchCount = touchCount + 1
    local colorIndex = (touchCount % #colors) + 1
    part.Color = colors[colorIndex]
    label.Text = "Touched " .. touchCount .. " times!"
end)

Press Play and touch the Part. Each touch cycles the color and increments the counter on the billboard.

What this script demonstrates:

  • Creating UI elements (BillboardGui, TextLabel) programmatically
  • Handling the Touched event
  • Modifying Part properties (Color)
  • Using arithmetic with the modulo operator (%) to cycle through a list
  • The distinction between server scripts (this one) and LocalScripts

Next Steps

You now know the fundamentals of Luau. Here is where to go from here:

  • DataStores: Save and load player data between sessions (money, inventory, progress).
  • RemoteEvents: Send messages between server scripts and LocalScripts to synchronize state.
  • UserInputService: Handle keyboard, mouse, and gamepad input in LocalScripts.
  • TweenService: Smoothly animate parts, UI, and camera movements.

The official Luau documentation on the Roblox Creator Hub is an excellent reference as you build your skills.

See Also