5.7 Events

We’ve only worked with code that executes in a sequential manner, running from the top of the script to bottom or in some loop top to bottom. The program runs synchronously.

But that isn’t how the real world works. Things don’t wait in line nicely until its turn to talk. In software development and in video games especially, few things happen in a sequential manner.

If have a door that requires a key to open, it’s hardly optimal to create a continuous loop that checks for a key to be inserted and then to open the door—a technique known as polling.

So we need a different paradigm of programming to reflect this. That way is by means of events.

An event is any process that is generated outside of the sequential flow of a program. Usually these events originate from a source outside of the current script and asynchronously.

The player may have clicked a button on their controller, a character just touched an object, or a timer runs out.


Because video games are interactive and highly dependent on states in a game world where we cannot know when some action will occur yet that occurrence still needs to be processed promptly, video games are highly event driven.

In the Roblox game engine, events are triggering all the time and generating a signal. They just get discarded because we don’t assign code to process those events.

To process an event, we need to “connect” to it a listener. This is nothing more than a function you define that will be invoked when the event occurs.

Parenting objects to a folder for example. From the API for the Folder object, if you scroll down to the ‘Events’ section (you may need to click the ‘View all inherited from Instance’ link), you will find an event called ‘ChildAdded.’ This event is inherited from the Instance class and fires anytime another object is parented to the folder. A similar one fires when a child object is removed, another for if a descendant is added at any level, etc.


There are many different types of events that may generate a signal, but the manner of connecting a listener almost always takes the same form.

Add a folder to the workspace and add this in a script.

Lua
local folder = game.Workspace.Folder

folder.ChildAdded:Connect(function(child) --Connect a listener to the event
	print(child)
end)

task.wait(5)

for i = 1, 5 do
	local part = Instance.new("Part")
	part.Name = "Part " .. i
	part.Parent = folder
	task.wait(1)
end
--Output: Part 1
--Output: Part 2
--Output: Part 3
--Output: Part 4
--Output: Part 5

In line 3, we indexed the ChildAdded event and “connect” a listener.

More simply put, call the method Connect and pass it the function that we want to run when the event takes place. In this case, our function is an anonymous function that just prints out the added child.

Depending on the event, arguments are often also passed into the function when it is called. For the ChildAdded event, the newly added object is passed as the function argument.


It is not a requirement that events only be connected to anonymous functions.

If you have a function that will be used more than once or a long function, you might choose to create a named function instead. Then you just pass the function name when to connect the event.

Lua
local function handleChildAdded(child)
	print(child)
end

local folder = game.Workspace.Folder
folder.ChildAdded:Connect(handleChildAdded) --only the function name is passed

task.wait(5)

for i = 1, 5 do
	local part = Instance.new("Part")
	part.Name =  "Part " .. i
	part.Parent = folder
	task.wait(1)
end

This will likely prevent the named function from accessing any lexically scoped variables. If your function requires those variables, you can create an intermediate anonymous function that calls the named function and pass those variables as arguments.


Disconnecting

You might not want an event to fire indefinitely or maybe you only want to it fire once.

It would be very limiting if there were not a way to disconnect a listener after we were done with some task.

Whenever you connect to an event, an RBXScriptConnection is returned. This is usually just referred to as the “connection.” In it there is one property indicating the status of the connection and a Disconnect method, used to…well, disconnect it.

Lua
local folder = game.Workspace.Folder
local connection = folder.ChildAdded:Connect(function(child)
	print(child)
end)

task.wait(5)

for i = 1, 5 do
	local part = Instance.new("Part")
	part.Name = "Part " .. i
	part.Parent = folder
	task.wait(1)
	
	if i >= 3 then
		connection:Disconnect()
		connection = nil
	end
end
--Output: Part 1
--Output: Part 2
--Output: Part 3

After the Disconnect method has been called, it is not possible to reconnect using that connection, so it’s good practice to set the variable to nil.


When a listener fires, it spawns a new thread that runs independent of the script level thread and any other thread originating from that same exact signal.

Lua
local folder = game.Workspace.Folder

folder.ChildAdded:Connect(function(child) --first handler
	print("Listener 1:", child)
end)

folder.ChildAdded:Connect(function(child) --second handler
	local waitTime = math.random()
	task.wait(waitTime)
	print(child, "Waited", waitTime)
end)

task.wait(5)

for i = 1, 5 do
	local part = Instance.new("Part")
	part.Name = "Part " .. i
	part.Parent = folder
end

--Output: Part Number 3
--Output: Part Number 1
--Output: Part Number 5
--Output: Part Number 4
--Output: Part Number 2
--Output: Part 5 Waited 0.0738862112934596
--Output: Part 1 Waited 0.4352729044338872
--Output: Part 4 Waited 0.46942010740389134
--Output: Part 2 Waited 0.5449507795670377
--Output: Part 3 Waited 0.9249466262110766

Here we connect two functions to the ChildAdded event of the same folder and remove the wait in the for loop. This makes it such that all five parts are added nearly at the same time but still in order.

For the second listener a random wait is applied. Because each thread has to wait a different amount of time and finishes at different times, they print the parts in some undefined order but none of the threads have to wait for another.

In this example a total of ten threads were spawned and executed independent of each other.


One of the most common uses of events is to detect when a part has been touched.

If we look at the API for (base)parts, there is a Touched event that passes the other touching part to the listener. Using that information, we can find the character that touched some part ingame.

First, add a part into the workspace if you do not already have one there. This will also require a play session since we need physics simulation.

Lua
local part = game.Workspace.Part

part.Touched:Connect(function(otherPart)
	local character = otherPart.Parent
	
	if character.ClassName == "Model" then
		print("Detected:", character, otherPart)
	end
end)

Now start the session and go walk all over that part. And we get….

Well, we get kind of a mess actually.

Lua
--Detected: {Character_Name_Here, LeftFoot}
--Detected: {Character_Name_Here, LeftLowerLeg}
--Detected: {Character_Name_Here, RightLowerLeg}
--Detected: {Character_Name_Here, LeftUpperLeg}
--Detected: {Character_Name_Here, RightUpperLeg}
--Detected: {Character_Name_Here, LeftLowerLeg}
--Detected: {Character_Name_Here, LeftFoot}
--Detected: {Character_Name_Here, HumanoidRootPart}
--Detected: {Character_Name_Here, RightFoot}
--Detected: {Character_Name_Here, LeftFoot}