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 
Have fun with your fully functional Lua events!