Handling events in a ModuleScript, should I make a custom handler or use strong references to bindables?

I’m interested in setting up signals for a few event-driven modules within my game to mimic the style of an RBXScriptSignal and to test my waters with putting different paradigms to practice. I’ve also seen this being used in the Lua Chat System, which led me to make an inquiry regarding this.

Should I hold strong references to BindableEvents without parenting them to the DataModel, or make my own custom event handler? What are the benefits or caveats of each, besides replication? (replication is honestly going to be a bit of a pain to handle)

Essentially what I’m thinking for strong references is that a ModuleScript will serve as the “internal backend”. It would expose the bindables along with other functionality so that other scripts can use Event:Connect to hook a function. When the backend calls fire on the bindable, then of course those connected functions would fire.

4 Likes

To me, a custom event handler is easy enough to make:

-- Connection Class, hidden inside the Event Class

local Connection = {}
Connection.__index = Connection

function Connection.new(listeners)
	local self = {
		Connected = true,
		Listeners = listeners
	}
	
	return setmetatable(self, Connection)
end

function Connection:Disconnect()
	self.Connected = false
	self.Listeners[self] = nil
end
-- The main dish: Event Class

local RunService = game:GetService("RunService")

local Connection = require(script.Connection)

local Event = {}
Event.__index = Event

function Event.new()
	local self = {
		Listeners = {},
		Waiting = {}
	}
	
	return setmetatable(self, Event)
end

function Event:Fire(...)
	for _, fn in pairs(self.Listeners) do
		coroutine.wrap(fn)(...)
	end
	
	local oldWaiting = self.Waiting
	self.Waiting = {}
	for thread in pairs(oldWaiting) do
		coroutine.resume(thread, ...)
	end
end

function Event:Connect(fn)
	local connection = Connection.new(self.Listeners)
	self.Listeners[connection] = fn
	return connection
end

function Event:Wait(timeout, default)
	local thread = coroutine.running()
	self.Waiting[thread] = true
	
	if timeout then
		coroutine.wrap(function()
			local i = 0
			while i < timeout do
				i += RunService.Heartbeat:Wait()
			end
			self.Waiting[thread] = nil
			coroutine.resume(thread, default)
		end)()
	end
	
	return coroutine.yield()
end

return Event
-- some example code:

local myEvent = Event.new()
local connection = myEvent:Connect(function(message)
	print(message)
end)
myEvent:Fire("hello world!") --> hello world!

connection:Disconnect()
myEvent:Fire("forever alone,,,") -- nothing

delay(2, function()
	myEvent:Fire("I waited 7 years for this!")
end)

local newMsg = myEvent:Wait() -- waits for the message

print(newMsg) --> I waited 7 years...

delay(60, function()
	myEvent:Fire("I waited a whole minute fo-")
end)

local newMsg2 = myEvent:Wait(3, "I waited 3 seconds for this!")

print(newMsg2)

If you store all your lua events in a ModuleScript, you have basically achieved the same goal as the BindableEvent has, just with a more flexible interface. RemoteEvents, and stack tracing I believe, are where the similarities stop.

Edit 1: Just changed it so that two events can’t have the same listeners.

Edit 2: Now I pretty much guaranteed it by not having it in the class to begin with.

I just realized that there is no way to have a good ol’ :Wait without transforming your code some, I guess the only way this would happen is if you wrapped your entire script in a coroutine and called coroutine.yield upon wait or something hacky with setfenv somehow. Of course you can just poll until the wait is done, but that’s just bad practice.

Edit 3: I figured out how to do that up there by the way via the coroutine.running method :man_facepalming:
Have fun with your fully functional Lua events!

9 Likes