Lua Quick-Start Guide

a guide written by evaera

Lua is a lightweight, dynamically typed scripting language. It is implemented into 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 Absolute Beginner's Guide to Scripting.

Syntax & Basics

Lua uses a very 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 /* .. */
]]

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 will find very few circumstances where you actually need a global variable. You can define a local variable with the local keyword. You don't need to declare a value for the variable to declare it. 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 separately.

local my_var = 1
local my_second_var

Variables 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 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("Alfa")
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 simply list of key-value pairs. The key and value can be 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 you cannot use this if the string starts with a number.

Every value in a table is considered to be 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.

Note In Lua, tables with numerical keys start from index 1, not 0. This is something that Lua is (in)famous for and is one of the things that causes the most headache for programmers.

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

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

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

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

local myFourthTable = {
    ["a"] = "Alfa";
    ["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 = "alfa"

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("alfa" == "alfa") --> 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

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 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 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.

The repeat loop is similar to the while loop except the condition is checked at the end of the block instead of at the beginning. This means that the code inside will always run at least once. The repeat loop is generally avoided in Lua development because its use case is limited and it can get difficult to read because it doesn't use the standard do..end syntax.

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

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

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

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.

The first declaration is global as there is no local keyword. Generally, global functions are considered to be okay. However, you can still make them local if you want, either by putting a local in front or by declaring the variable before you define the function.

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

function myFunction(param1, param2)

end

local mySecondFunction = function(param1, param2)

end

local Pizza = mySecondFunction

print(Pizza == mySecondFunction) --> true

Pizza(1, 2)

You can accept as many arguments as you want with the ... 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 actual ... object is a tuple, so you need to surround it by curly braces to make it into a table. A tuple is simply a list of names, such as the parameters in a function. Functions can return multiple values, which is also a tuple. You can assign variables to the return values.

If you put a function in a table, you can call it in two different ways. The standard dot syntax:

local myTable = {}

function myTable.Dance(p1, p2)
    print("dancing")
    print(p1)
    print(p2)
end

function myTable:StopDancing(self)

end

myTable.Dance("Piano") --> dancing, Piano, nil

Or, the colon syntax, which will send the table itself as the first argument. Note that the function can be defined with either syntax and you can call any function in a table both ways.

myTable:Dance("Piano") --> dancing, table: 0x8c6f30, "Piano"

If you try to print a table, it will print "table" followed by the memory address of the table.

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 a full list of global namespace functions here, and you should definitely read this!

There are also two tables that are shared between all scripts on the server or the client (but not both), _G and shared. _G does not work the same as it does in normal (non-ROBLOX) Lua. _G is simply a truly global table (across separate scripts), so if you want to get or set values from it you must always use _G.something.

_G.hello = "Hello"

In another script:

wait(1)
print(_G.hello) -->Hello

Note that the second script is waiting 1 second before running because scripts in ROBLOX can begin running at different times and we want to make sure that the "hello" key is set in _G before we try to print it.

If you want to store data in the game tree, look into using the value objects, such as StringValue, ObjectValue, and NumberValue.

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 ]].

local a = "Alfa"
local b = 'Afa'
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 will find more information on strings here.

ROBLOX 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 the syntax 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 functions. 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 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 minimum length of a yield is one render frame, because the thread scheduler won't resume any tasks until the next frame starts.

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 won't be far off.

You can, however, use coroutines in Lua, which essentially create what acts like a different script in the task scheduler (but still shares the same scope). This means that, in a sense, you can run two blocks of code at relatively the same time, assuming that one yields. You can create a "poor-man's" coroutine by using the spawn function, which spawns a function and executes it immediately, outside of the current script's task.

spawn(function()
    for i=1, 10 do
        print("First " .. i)
    end
end)

for i=1, 10 do
    print("Second " .. i)
end

First 1

First 2

First 3

[etc]

Second 1

Second 2

Second 3

[etc]

However, if you cause them both to yield (in this case, using wait)

spawn(function()
    for i=1, 10 do
        print("First " .. i)
        wait() -- using wait without any arguments waits the shortest amount of time (1 frame)
    end
end)

for i=1, 10 do
    print("Second " .. i)
    wait()
end

First 1

Second 1

First 2

Second 2

First 3

Second 3

[etc]

The above code blocks demonstrate how the task scheduler works in ROBLOX. The thread scheduler will resume the next block of code before resuming the original one.

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 Wiki for more information.