Scripting Helpers is winding down operations and is now read-only. More info→

Lua Quick-Start Guide

a guide written by evaera

Lua is a lightweight, dynamically typed embeddable scripting language. It is tied in to Roblox game development very closely. This guide is for experienced programmers who want a quick-start guide for using Lua in Roblox.

If this guide is too brief or advanced for you, you might want to check out the Official Lua 5.1 Manual.

Syntax & Basics

Lua uses a simplistic syntax. You don't need to use semicolons or significant whitespace. You can use semicolons if you want, because they are ignored by the interpreter.

print("Hello, world!") -- This is a short comment, similar to // in other languages

--[[
    This is a long comment, similar to /* .. */
]]

Lua functions can be called without parenthesis only if there is only one parameter and it is a string or table literal.

print "Hello, world!"

func{1, 2, 3}

Variables

All variables in Lua are global by default. Therefore, you should always indicate you want your variables to be local to the current scope whenever you first declare variables. You can declare a local variable with the local keyword. It is good practice to always use the local keyword when you first declare a variable (even in the top-most scope) so that it is clear that a new variable is being introduced.

You can declare variables without initializing them. In Roblox, global variables mean global to the current script context, so they are not global to every script in your game. Each script in Roblox runs with a separate global scope.

local declaredVariable
local initializedVariable = 5

Identifiers can contain any sequence of numbers, letters, and underscores, but they cannot begin with a number.

Null is represented by the nil keyword. All variables are nil unless otherwise defined. Undeclared variables are indistinguishable from declared variables that have no value.

Code blocks and if statements

Code blocks are defined by certain keywords followed by an end keyword somewhere afterwards. Conditions in if statements must be followed by a then keyword. if statements use elseif, not else if. else doesn't use a then.

if a == 1 then
    print("alpha")
elseif a == 2 then
    print("Bravo")
else
    print("Charlie")
end

You can create a new empty scope with a do...end block.

local a = 1
do
    local b = 2
    c = 3 -- global variable. Don't do this! This variable is escaping the scope.

    print(a) --> 1
    print(b) --> 2
    print(c) --> 3
end
print(a) --> 1
print(b) --> nil
print(c) --> 3

Tables

Lua uses a singular data type called a Table to serve the purpose of arrays, dictionaries, hash maps, objects, etc from other languages. Tables are maps of key-value pairs. The key and value can both be of any data type, even other tables. You could even use a boolean value as a key if you wanted to. As Lua is dynamically typed, you can have any mixture of data types in both keys and values as you wish.

Table values can be defined both inline and separately. There are generally two ways to get and set table values, the "dot" syntax (e.g. myTable.a) and the bracket syntax (e.g. myTable["a"]. Note that you can only use the dot syntax when using string keys and the key meets the Lua identifier rules.

Every value in a table is considered nil unless it is set. This means there is no way to tell if a value in a table has been declared and it's just nil, or if it's never been set before. This means that if you define keys in a table and set them to nil, it will have no effect at all and will be like you didn't even define them.

Under the hood, Lua tables are separated into two parts: a hash map and an array. Thus, positive numerical indexes have special optimization and also work with Lua's length operator.

Note In Lua, tables with numerical keys start from index 1, not 0.

You can separate members in a table literal by either a comma , or a semicolon ;.

local myTable = {}
myTable[1] = "alpha"
myTable[2] = "Bravo"
myTable[3] = "Charlie"

It is more efficient for memory allocation to define a table in the literal form:

local mySecondTable = {
    "alpha",
    "Bravo",
    "Charlie"
}

local myThirdTable = {}
myThirdTable["a"] = "alpha"
myThirdTable.b = "Bravo"

local myFourthTable = {
    ["a"] = "alpha";
    ["b"] = "Bravo";
}

local mixed = {
    "hello",
    2,
    3,
    true,
    [99] = "goodbye",
    ["hello"] = 1
}

mixed[true] = "greetings."
mixed[mixed] = "hi"

Primitive and complex data types

Tables are considered a complex data type in Lua, so that means that variables simply refer to the object in memory. If you set a variable to a table that already exists, the two variables will refer to the same table in memory. You also cannot directly compare two table values and expect them to check anything other than if they refer to the same object in memory.

local t1 = {}
t1.a = "alpha"

local t2 = t1

t2.b = "bravo"

print(t1.b) --> bravo

print(t1 == t2) --> true

print({} == {}) --> false

However, strings, numbers, booleans, and other primitive types are just that, and can be compared directly.

print("alpha" == "alpha") --> true
print(1 == 1) --> true
print("Bravo" == "bravo") --> false

Numbers in Lua are only one type, number. There aren't different types like int, float, long, double, etc like on other languages. Technically, all numbers in Lua are just double precision floating point numbers.

Operators

Math

print(1 * 1) -- Mult
print(1 + 1) -- Add
print(1 / 1) -- Divide -- fun fact: Dividing by 0 in Lua will evaluate to infinity. 
print(1 - 1) -- Subtract
print(1 % 1) -- Modulus 
print(1 ^ 1) -- Exponentiation 

Comparison

== -- equal to
~= -- not equal to (!= doesn't work in Lua)
> --greater
< --less
>= --greater or equal
<= -- less or equal

Definition and incrementing

Lua only has a = operator for definition and incrementing. Lua does not have +=, -=, ++, or anything else.

local a = 3
while a < 10 do
    a = a + 1
end

Concatenation

The concatenation operator in Lua is .. . You can concatenate strings and numbers without issue. However, you can't concatenate other types like booleans or tables.

local world = "World"
print("Hello " .. world .. "!")

Length

You can get the length of a table or a string with the # operator. Note that this only works for tables that have numerical indexes. It won't work with tables that have string indexes, for example.

local t = {1, 2, 3, 4, true, false, "Hot Potato"}
print(#t) --> 6

local str = "Hello"
print(#str) --> 5

The table length operator is only guaranteed to return an index in the array part of a table for which the next index is nil. For example, the length of the table:

print(#{1, 2, 3, 4, nil, 1, nil, nil, nil, nil}) --> 4
print(#{1, 2, 3, 4, nil, 1, nil, nil, nil, nil, 1, nil}) --> 6

Loops

There are four kinds of loops. While, numeric for, generic for, and repeat.

A while loop is the simplest kind of loop and functions generally as you would expect in other languages, meaning that the condition is checked before the inner block is run.

A repeat loop is similar to a do..while loop from other languages. It is like a while loop, except its condition si checked after the first iteration. The repeat loop uses nonstandard syntax so that the interpreter can avoid arbitrary lookahead, because the authors of Lua wanted a single-pass-to-bytecode compiler.

A numeric for loop deals with a variable local to the loop scope and either increments or decrements it. You declare and define the variable you want to use, followed by the goal value. There is a third, optional value you can use that is the increment. If you omit this it will be 1.

A generic for loop is a for loop that uses an iterator function. The function is called every iteration of the loop and it returns the values to be used in that iteration. pairs is an iterator function provided by default in the global scope. It takes a table as an argument and returns the index and the value of every value in the table. It will even iterate over non-numerical indexes. It has a brother, ipairs, which does exactly the same thing except it only works with numerical indexes and stops once it hits a nil value.

-- While loop
while a == 1 do
    print("Delta")
end

-- Repeat loop
repeat
    print("Golf")
until a == 1

-- Numeric for loop
for a = 1, 10 do
    print("Echo " .. a)
end

-- Numeric for loop with increment set
for b = 10, 1, -1 do
    print("Foxtrot " .. b)
end

-- Generic for loop
for a, b in pairs(some_table) do 
    print(a, b)
end

Functions

Functions are another complex data type and you can pass them around as variables, but you can also call them and execute the code on the inside. These are similar to methods in other languages. There are two ways to define a function in Lua: function declarations and function expressions.

Functions are just like any other data type in that you can pass them around and assign them to variables.

local function myFunction(param1, param2)

end

local mySecondFunction = function(param1, param2)

end

local foo = mySecondFunction

print(foo == mySecondFunction) --> true

foo(1, 2)

You can accept as many arguments as you want with the vararg syntax:

function lotsOfArguments(...)
    local args = {...}

    print(args[1])

    return "hi", "bye", 2, 3
end

local r1, r2, r3, r4 = lotsOfArguments(1, 2, 3, 4, "Bravo", false)

The vararg expression (...) results in an uncontained list of values (sometimes referred to as a tuple), so you need to surround it with curly braces to wrap it in a table.

Functions can also return multiple values, which is also referred to as a tuple. You can assign variables to the return values:

function returnsMultiple()
    return 1, 2, 3
end

local foo, bar, baz = returnsMultiple()

Note that tuple is not an actual data type in Lua, and the term is simply used to refer to an uncontained list of multiple values.

Functions in tables have a shorthand operator in order to send and receive the table itself as the first parameter (implicitly becoming self).

local cake = {}

----
function cake:eat(numberOfPieces)

end
-- equivalent:
function cake.eat(self, numberofPieces)

end
----

Likewise, calling a function in a table with : will send the table as the first parameter.

----
cake:eat(5)
-- equivalent:
cake.eat(cake, 5)
----

Global namespace

Lua provides several tables in the global namespace that contain utility functions, such as string, table, and math. As such, you should never name any variable string or table, as you will override the tables that contain useful functions.

You can find the list here and here.

Strings

Lua has a number of ways to define a string literal. Double and single quoted strings are standard. You can define multi-line strings in Lua by using double square brackets. You can also put any number of equals signs (=) in between the square brackets ([===[ ... ]===]), and as long as they are balanced you can include sequences inside of them like ]].

Multi-line string literals do not allow escape sequences.

local a = "Alpha"
local b = 'Bravo'
local c = [[ Charlie ]]
local d = [[
    Delta
    Delta
    Delta
]]
local e = [==[
    Echo
    Echo
    Echo
]==]

Lua also has its own host of built-in string manipulation functions and its own, watered-down version of Regular Expressions called string patterns. You can find more information here

Roblox data types

Roblox introduces data types into Lua as a type called "userdata". A userdata is a type that is implemented C-side, or by the Roblox engine.

An example of userdata types are Vector3, CFrame, Parts, Models, Color3, BrickColor, etc. You can create a userdata by using their respective constructor functions which follow the convention Vector3.new(1, 2, 3), Instance.new("Model"), etc. These are tables that are provided by Roblox that contain a function called new, which is implemented in the engine and returns the newly created type.

Some userdata are special in that they have custom comparison overrides. For example, you can compare two Vector3 types without worrying about memory addresses. It will only compare the coordinates. You will usually find this behavior in userdata that are for storing data, like BrickColor, Vector3, CFrame, Color3, Vector2, Region3, etc.

print(Vector3.new(1, 2, 3) == Vector3.new(1, 2, 3)) --> true

However, this is not the case for Instances (world objects):

print(Instance.new("Part") == Instance.new("Part")) --> false

Userdata generally have properties and methods you can set and call, respectively. For example, if you have a Part in Workspace, you can set its Transparency property like this:

game.Workspace.Part.Transparency = 0.5

game refers to the root of the game world.

You can also remove the part by calling its Destroy method.

game.Workspace.Part:Destroy()

You can use the Roblox wiki to find all of the properties and methods for every type of userdata. Instances are a special type of userdata that represent objects in the game world or objects that affect the game in some way, and are always visible in the game tree in the Explorer. Instances use an inheritance system, so all instances inherit all methods and properties from the Instance base class, such as the Name property or the Destroy method.

Another example is how all part types, e.g. Part, WedgePart, inherit from the BasePart class. Not all instance types are creatable, however, as BasePart isn't a creatable instance, just as the Instance class isn't directly creatable.

To create a new part and set some properties, you can do this:

local part = Instance.new("Part")
part.Parent = game.Workspace
part.Name = "myPart"
part.Anchored = true
part.BrickColor = BrickColor.new("Really red") -- note we are creating a new BrickColor userdata here and setting it to the BrickColor property of Part which is of the BrickColor type

You can see a full of list of all Lua and userdata types here.

Multiple scripts, concurrent code, and coroutines

In Roblox Lua, all code runs in one actual thread. This means that all scripts in the game will run sequentially unless they yield. If a script yields, then it will be resumed when the yielding function returns. This means that other scripts will start to execute immediately after one script yields.

The most commonly used function to yield or sleep the current block is wait. It takes the amount of time you want to yield as the first argument in seconds. It then returns the amount of time that was actually waited. They will seldom be the same, but they generally won't be far off. However, the time can increase significantly because thread resumption can be throttled if there is too much work being done.

Coroutines exist as a means of "green threading" in your own code. This means that, in a sense, you can run two blocks of code interweaved, assuming that one yields. For more information, see Beginners Guide to Coroutines

Server-Client relationship

You should read this guide to learn more about how the server-client relationship works in Roblox. This is a very important concept to understand before you dive in!

Conclusion

If you want to learn more, you should definitely check out the Official Roblox Developer Hub for more information.