Introduction to Scripting

Comments

In Lua, comments start with a --

-- This is a comment in Lua

Types

There are eight basic types in Lua.

  1. nil

  2. boolean

  3. number

  4. string

  5. userdata

  6. function

  7. thread

  8. table

nil

Nil is a type with a single value, nil, whose main property is to be different from any other value. As we have seen, a global variable has a nil value by default, before a first assignment, and you can assign nil to a global variable to delete it. Lua uses nil as a kind of non-value, to represent the absence of a useful value.

boolean

The boolean type has two values, false and true, which represent the traditional boolean values. However, they do not hold a monopoly of condition values: In Lua, any value may represent a condition. Conditionals (such as the ones in control structures) consider false and nil as false and anything else as true. Beware that, unlike some other scripting languages, Lua considers both zero and the empty string as true in conditional tests.

In Lua, true and false are in lowercase.

numbers

The number type represents real (double-precision floating-point) numbers. Lua has no integer type, as it does not need it.

strings

Strings have the usual meaning: a sequence of characters. Lua is eight-bit clean and so strings may contain characters with any numeric value, including embedded zeros. That means that you can store any binary data into a string. Strings in Lua are immutable values. You cannot change a character inside a string, as you may in C; instead, you create a new string with the desired modifications.

tables

The table type implements associative arrays. An associative array is an array that can be indexed not only with numbers, but also with strings or any other value of the language, except nil. Moreover, tables have no fixed size; you can add as many elements as you want to a table dynamically. Tables are the main (in fact, the only) data structuring mechanism in Lua, and a powerful one. We use tables to represent ordinary arrays, symbol tables, sets, records, queues, and other data structures, in a simple, uniform, and efficient way. Lua uses tables to represent packages as well. When we write io.read, we mean "the read entry from the io package". For Lua, that means "index the table io using the string "read" as the key".

Tables in Lua are neither values nor variables; they are objects. If you are familiar with arrays in Java or Scheme, then you have a fair idea of what we mean. However, if your idea of an array comes from C or Pascal, you have to open your mind a bit. You may think of a table as a dynamically allocated object; your program only manipulates references (or pointers) to them. There are no hidden copies or creation of new tables behind the scenes. Moreover, you do not have to declare a table in Lua; in fact, there is no way to declare one. You create tables by means of a constructor expression, which in its simplest form is written as {}

functions

Functions are first-class values in Lua. That means that functions can be stored in variables, passed as arguments to other functions, and returned as results. Such facilities give great flexibility to the language: A program may redefine a function to add new functionality, or simply erase a function to create a secure environment when running a piece of untrusted code (such as code received through a network). Moreover, Lua offers good support for functional programming, including nested functions with proper lexical scoping.

Userdata and Threads

The userdata type allows arbitrary C data to be stored in Lua variables. It has no predefined operations in Lua, except assignment and equality test. Userdata are used to represent new types created by an application program or a library written in C; for instance, the standard I/O library uses them to represent files.

Variables

A variable is essentially a name that can hold a value. Variable values can be numbers, strings, booleans, data types, and more.

Variable Naming

In Lua, variable names can be any non-reserved string of letters, digits, and underscores, but they cannot start with a digit:

LETTERS -- valid
a1 -- valid
var_name -- valid
_test -- valid
if -- NOT valid
25th -- NOT valid

Note that Lua is a case-sensitive language, meaning that TestVar and TESTVAR are two unique names. As a convention, names which begin with an underscore followed by uppercase letters (such as _VERSION) should be avoided, as they may be reserved for internal global Lua variables.

The following keywords are reserved by Lua and cannot be used as variable or function names:

  • and

  • break

  • do

  • else

  • elseif

  • end

  • false

  • for

  • function

  • if

  • in

  • local

  • nil

  • not

  • or

  • repeat

  • return

  • then

  • true

  • until

  • while

Assignment

Assigning a value to a variable is done with the = operator.

Lua
Output
Lua
x = 10
word = "Hello"
reference = workspace.Part
print(x)
print(word)
print(reference)
Output
10
Hello
Part

Remember that the variable is always on the left side of the = while the value is on the right side.

Once declared, a variable’s value can be changed by simply assigning another value to it:

Lua
Output
Lua
x = 10
print(x)
x = 1000
print(x)
Output
10
1000

Variable Scope

In Lua, variables exist in one of two possible scopes, global or local. All variables will default to global unless they are declared as local.

Global

A global variable is visible to all scopes of a script. In the following code, the global variable x starts at 0, increments by 1 in each iteration of the for loop, and is printed again afterward with a final value of 5.

Lua
Output
Lua
x = 0 -- Global variable "x"
for i = 1, 5 do
x = x + 1
print("Global 'x' = " .. x)
end
print("Global 'x' = " .. x)
Output
Global 'x' = 1
Global 'x' = 2
Global 'x' = 3
Global 'x' = 4
Global 'x' = 5
Global 'x' = 5

Local

A local variable is accessible only in the block where it’s declared. Local variables are declared using the local statement:

local x = 0 -- Local variable "x"

In the following example, the initial local variable x is set to 0, and another local variable x is declared inside the for loop. As the loop iterates, its locally-scoped x is printed with a constant value of 1, then the initial variable x is printed with an unchanged value of 0.

Lua
Output
Lua
local x = 0 -- Local variable "x"
for i = 1, 5 do
local x = 1 -- Different variable "x", local to this "for" loop
print("Loop 'x' = " .. x)
end
print("Initial 'x' = " .. x)
Output
Loop 'x' = 1
Loop 'x' = 1
Loop 'x' = 1
Loop 'x' = 1
Loop 'x' = 1
Initial 'x' = 0

Local variables are obtained faster than global variables because they're integrated into the environment in which they were created. If possible, you should always use local variables over global variables, unless there's a specific reason otherwise.

Control Structures

Lua provides a small and conventional set of control structures, with if for conditional and while, repeat, and for for iteration. All control structures have an explicit terminator: end terminates the if, for and while structures; and until terminates the repeat structure.

if then else

An if statement tests its condition and executes its then-part or its else-part accordingly. The else-part is optional.

Lua
Output
Lua
robux = 1000
if robux > 999 then
print("awesome!")
else
print("not enough!")
end
Output
awesome!

When you write nested ifs, you can use elseif. It is similar to an else followed by an if, but it avoids the need for multiple ends.

Lua
Output
Lua
robux = 500
if robux > 999 then
print("awesome!")
elseif robux > 499 then
print("not enough!")
end
Output
not enough!

while

As usual, Lua first tests the while condition; if the condition is false, then the loop ends; otherwise, Lua executes the body of the loop and repeats the process.

Lua
Output
Lua
local i = 1
while i < 10 do
print(i)
i = i + 1
end
Output
1
2
3
4
5
6
7
8
9

Functions

Functions are sets of instructions that can be used multiple times in a script. Once defined, a function can be executed through a command or triggered through an event.

Defining Functions

A basic function declaration includes the function keyword followed by the function name and a pair of parentheses (()). Since the function’s body will be a block of code, it must be closed with the end keyword.

local function moreMoney()
end

Between the () and end is where commands and other code make up the function body. These commands will be executed when the function is called:

local function moreMoney()
-- Function body
print("moreMoney Function called!")
end

Calling Functions

Once a function is defined, it can be executed by calling it (functions do not self-execute). To call a function, simply type its name followed by parentheses (()):

local function moreMoney()
-- Function body
print("moreMoney Function called!")
end
moreMoney()

Function Parameters

Functions can utilize parameters for data that will be passed in. When you declare a function, you may include one or more parameter names inside the parentheses:

-- "num1" and "num2" are parameters
local function moreMoney(num1, num2)
end

When calling a function with parameters, specify the values that should be passed to the function. For instance, the following function takes the two numbers passed in during each call, adds them together, and outputs the result:

local function moreMoney(num1, num2)
local result = num1 + num2
print(num1 .. " + " .. num2 .. " = " .. result)
end
moreMoney(2, 3)
moreMoney(4, 0.5)
moreMoney(1000, 500)

Function parameters are always local to the function and are only used in the function's scope and its descending scopes.

If you call a Lua function with more parameters than it's expecting, the excess ones will be ignored. Conversely, if the function expects more parameters than you provide, the value nil will be used for all missing parameters.

Returning Values

In addition to accepting parameters, functions can also return (send back) data to the calling command. This is done through return values. Working off the example above, the following function returns the sum instead of printing it:

local function moreMoney(num1, num2)
local myMoney = num1 + num2
return myMoney
end

Do not put any commands following a return command as doing so will generate an error.

When a function returns a value, it can be assigned to a variable or used wherever a variable can be used. The following code illustrates this concept:

local function moreMoney(num1, num2)
local myMoney = num1 + num2
return myMoney
end
-- Call the function and store the returned value in variables
local result1 = moreMoney(2, 3)
print(result1)
local result2 = moreMoney(1000, 1000)
print(result2)

Lua also lets you return multiple values from a function:

local function moreMoney(num1, num2)
local sum = num1 + num2
local difference = num1 - num2
return sum, difference
end
local sum, difference = moreMoney(2, 3)
print(sum)
print(difference)

Other Function Techniques

Event-Triggered Functions

Functions don’t always need to be called with a command — they can also be called through an event. We'll look at this later.

Anonymous Functions

Functions can be created anonymously, that is without assigning them a name. This is useful when you need to call a function from the result of another function or event, for instance a delay() call or a PlayerAdded event connection:

delay(2, function(exactTimeElapsed)
print(exactTimeElapsed)
end)
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(player)
print(player.Name .. " joined the game!")
end)

Note that anonymous functions still require an end closure before the ending ) of a containing function or event like delay() or :Connect().

Functions Within Tables

Since functions are a Lua data type, they can be stored in tables. This technique is commonly used in ModuleScripts where the module’s table contains various functions:

local MathModule = {}
function MathModule.addNumbers(num1, num2)
local sum = num1 + num2
return sum
end
function MathModule.subtractNumbers(num1, num2)
local difference = num1 - num2
return difference
end
return MathModule

Once contained in the module’s table, these functions can be called by any script which accesses the ModuleScript via require():

local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Require module
local MathModule = require(ReplicatedStorage:WaitForChild("MathModule"))
local sum = MathModule.addNumbers(2, 3)
print(sum)
local difference = MathModule.subtractNumbers(2, 3)
print(difference)

Handling Events

In addition to properties and functions, every object also has events which can be used to set up cause-and-effect systems. Events send out signals when specific things happen in a game, such as a player touching an object or a player connecting to the game. To fire an event is to have it send out such a signal.

Waiting for an Event

The Wait() function will cause the script to pause until the event occurs once. When it does, the function returns the data associated with the event’s firing.

local myPart = game.Workspace.Part
-- Wait until another part collides with "myPart"
local otherPart = myPart.Touched:Wait()
print("Part was touched by: " .. otherPart.Name)

Connecting a Function

The Connect() function can be used when a given function should run every time an event fires. This function immediately returns a connection object. In the example below, we connect a function, onTouched(), to a Part in the Workspace. If another part collides with myPart, the script prints the name of the other part involved in the collision.

local myPart = game.Workspace.Part
local function onTouched(otherPart)
print("Part was touched by: " .. otherPart.Name)
end
myPart.Touched:Connect(onTouched)

Disconnecting a Function

Eventually, you may no longer need a connected function to run when an event fires. To disconnect it, use the Disconnect() method of the connection object returned by Connect().

local points = Instance.new("NumberValue")
points.Name = "Points"
points.Value = 0
local connection
local function onPointsChanged(newPoints)
print("Points: " .. newPoints)
if newPoints >= 50 then
-- Stop listening for changes if we have at least 50 points
connection:Disconnect()
end
end
connection = points.Changed:Connect(onPointsChanged)
-- Trigger some changes
points.Value = 25
points.Value = 100
points.Value = 0 -- Prints nothing because we called Disconnect()

Always call Disconnect() when a connection is no longer needed. Forgetting to do so can cause your game to use more resources than necessary. Note, however, that this may not always be necessary; when an object is destroyed, all connections to that object’s events are disconnected automatically.

Event Data

Almost every event in Roblox will send data relevant to the event’s occurrence. For example, when a Player joins the game, the Players.PlayerAdded event fires with a reference to the new player.

local Players = game:GetService("Players")
local function onPlayerAdded(player)
print(player.Name .. " joined the game")
end
Players.PlayerAdded:Connect(onPlayerAdded)

Nested Connections

Sometimes you will need to connect a function to an event on an object provided by another event. To do so, define a second local function within the function connected to the first event.

A common example is detecting when a player’s character is spawned into the game. For this, you’ll need to access the CharacterAdded event of the Player involved in the Players.PlayerAdded event.

local Players = game:GetService("Players")
local function onPlayerAdded(player)
local function onCharacterAdded(character)
print(player.Name .. " spawned in: " .. character:GetFullName())
end
player.CharacterAdded:Connect(onCharacterAdded)
end
Players.PlayerAdded:Connect(onPlayerAdded)

Note that when a player leaves the game, their Player object is destroyed. This causes the connection to CharacterAdded made inside onPlayerAdded() to be disconnected, so you don't need to call Disconnect() in this case. In other circumstances where the object in question is not necessarily destroyed, you may need to use a table to keep track of connections to be disconnected later via another function.

Custom Game Events Using BindableEvent

Often times, you will need to define your own events for gameplay, such as when a team scores a goal. You might have one script award that team a point and another script launch some fireworks. For this, you’ll need to use a BindableEvent which features a single user-fireable event named Event.

You can either use a BindableEvent that already exists in the game hierarchy or create one using Instance.new(). Then, call Fire with any relevant data to trigger the Event. For example:

-- Create or get reference to a BindableEvent
local beGoal = Instance.new("BindableEvent")
-- Connect a function to the custom event
local function onGoalScored(team)
team.Score = team.Score + 1
end
beGoal.Event:Connect(onGoalScored)
-- Somewhere else in your code, fire the event
local Teams = game:GetService("Teams")
local redTeam = Teams["Red Team"]
beGoal:Fire(redTeam)

Simple data like strings, numbers, booleans, simple tables, and Roblox object references can be sent as event data through BindableEvent, but more complicated data such as functions or tables with metatables will require a more custom solution. See the documentation for BindableEvent:Fire() for details on the kinds of data that can be sent when firing bindable events.

Client-Server Events

Roblox runs on a client-server model. As such, some events that fire on the server will replicate and also fire on the client. This depends on the event, but when a game needs to define a custom event to be replicated from server-to-client or vice-versa, a RemoteEvent can be used. This works similarly to BindableEvent but is network-ready.