Event Systems in Roblox Lua (Part 3 – Better Solution)

Click here for part 2 of this tutorial series. In part 2, we created a simple solution to our problem of creating an event system. We saw there are some problems with this, namely the synchronous execution. One after another, our connected functions are called when the event is fired.

Often times, when you’re using a game engine it’s best to let the engine do the heavy lifting for you. Roblox provides the BindableEvent object to create events in (or out of) the game hierarchy, however it isn’t often that you’ll want to instantiate one every time you want to create a custom event. Let’s create an Event class which uses BindableEvent internally intstead. Let’s re-start with the boilerplate from part 2:

-- A new Event class that uses BindableEvent internally
local Event = {}
Event.__index = Event

function Event.new()
	local self = setmetatable({
		bindableEvent = Instance.new("BindableEvent");
	}, Event)
	return Event
end

We’re no longer keeping track of the connections ourselves with a table. We’ll merely be passing the connect and fire behaviors onto the already existing functionality of the object. This is quite simple, actually:

function Event:connect(func)
	return self.bindableEvent.Event:connect(func)
end

function Event:fire(...)
	self.bindableEvent:Fire(...)
end

The key benefit here: since we’re delegating the responsibility of calling the connected functions to the BindableEvent, it can handle the threading as well. Connected functions that yield will not block other connected functions, so this is a primary benefit from the first simple solution. Note the use of   to represent “any and all variables passed to this function”. We’ll talk more about it in part 4.

There is still a problem with this, though. BindableEvents were originally created for inter-script communication. When it comes to tables, there’s some “gotchas”

Avoid passing a mixed table (some values indexed by number and others by key), as only the data indexed by number will be passed.

BindableEvent documentation

This is a big drawback to using a BindableEvent. Your tables will not be the same table as the one you fired the event with. Here’s an example demonstrating this drawback:

local event = Event.new()
local my_table = {}

local function onEvent(other_table)
	error(other_table == my_table, "Fail whale!")
end

event:connect(onEvent)
event:fire(my_table)

You can see that we are firing the event with my_table , then in the connected function comparing that table to (supposedly) itself. This shouldn’t ever throw an error, but it always does due to the described limitations of the BindableEvent.

You’ll also lose metatables, too! So if you created an object using our object-oriented tricks from the previous tutorial, you won’t have an object anymore. Just an empty shell of a table, no purpose, no meaning in life… definitely something you want to avoid.

You can see that the key benefit of letting Roblox handle the threading with the cost of the BindableEvent’s limitations is definitely a trade-off. You can download the finished version of this Event class here. Is it possible to work around this limitation? Maybe. Click here to find out in part 4.

Author: Ozzypig

Roblox developer and computer scientist. I love all things coding and game design!