Is it possible to make a custom RBXScriptSignal with OOP?

If there is already a thread for this with a solution, please direct me to it.

I’m getting into OOP in lua, and I understand that you can almost create your own class with functions and properties. However, I haven’t found any way to create events for your class. Is it possible, and if so how?

4 Likes

The only true way to do it is with a deprecated function. There are Lua remakes of RBXScriptSignal though.

14 Likes

What’s the function?

Maybe something like this so you don’t have to use a BindableEvent

local class = {
    Events = {"SomeEvent1"}
}

--Create Events
function class:Init(self)

    --Events
    for _, eventName in pairs(self.Events) do
        local event = {Functions = {}}
        function event:Connect(func)
            table.insert(self.Functions, func)
        end
        function event:Fire()
            for _, func in pairs(event.Functions) do
                spawn(func)
            end
        end
        self[eventName] = event
    end
end

--Some other place
local object = ObjectService.new("TestObject")
object.SomeEvent1:Connect(function()
 print(1+2)
end)

object.SomeEvent1:Fire()
4 Likes

Nevermind. That one worked!

Here’s the one I used in one of my projects. No dependencies, unlike the above snippet. Note that it’s not possible to implement wait() without the use of BindableEvents or similar constructs.

local Connection = {}
Connection.__index = Connection

function Connection.new(signal, func)
	local self = {
		signal = signal,
		func = func,
	}
	setmetatable(self, Connection)

	return self
end

function Connection:disconnect()
	if self.signal then
		self.signal.connections[self.func] = nil
		self.signal = nil
		self.func = nil
	end
end

local Signal = {}
Signal.__index = Signal

function Signal.new()
	local self = {
		connections = {},
	}
	setmetatable(self, Signal)

	return self
end

function Signal:fire(...)
	for func,_ in pairs(self.connections) do
		func(...)
	end
end

function Signal:connect(func)
	self.connections[func] = true
	return Connection.new(self, func)
end

return Signal

Edit (August 2021): I made this post more than 3 years ago. Since then, I’ve learned that this approach has the following disadvantages:

  • Any errors in the callbacks will propagate to the thread firing the signal, and stop all handlers from being processed.
  • Any callback that yields will also yield the thread firing the signal. Using coroutine.wrap() will fix this.
  • Incorrect handling of reentrancy (the signal gets fired inside of a callback from firing the signal).

Additionally, since I wrote this the coroutine.running() API was introduced, making it possible to implement :wait().

Recently, the task.spawn and task.defer APIs were introduced, which helps make this even better. It’s possible to make a Signal class now that avoids all these downsides.

Check out this post for more info, as well as a fully featured signal library.

26 Likes

To note, my snippet didn’t need requirements from reading the code - it just so happened to be apart of the much larger Nevermore Engine.

I was referring to gillern’s snippet. It appears to rely on some unspecified OOP framework. The Nevermore one is good, I mainly linked mine as an alternative version which didn’t use BindableEvent.

3 Likes

Here’s my implementation.

Features:

  • API compatible with normal events.
  • Unserialized; can pass tables, functions, etc.
  • No coroutines; compatible with the Roblox scheduler.
  • Avoids common pitfalls such as holding argument references indefinitely, and arguments being overwritten in certain cases.
3 Likes

Yeah, ObjectService isn’t an official Roblox Service.

My game Shard Seekers can create thousands of signal-related objects, so I tried to avoid packing in unneeded features and went for something simple and lightweight. The resulting object is a list of functions with a metatable, which uses the least amount of memory possible for a Lua-based signal.



local setmetatable = setmetatable
local table_remove = table.remove

local Class = {}
local Me = {} -- 'Me' is short for 'Methods'
local Mt = {__index = Me} -- 'Mt' is short for 'Metatable'
-- Although hacky, it's possible to have the metatable change based on the number of arguments in the list for better firing performance. I left that out for simplicity.

-- 'cn' is short for 'connection'
-- I use 'connection()' instead of 'connection:Disconnect()'. This means I can use functions and connections interchangeably
local CnMt = {__call = function(cn)
	local self, method = cn[1], cn[2]
	for i = 1, #self do
		if self[i] == method then
			local sync = self[0]
			if sync then
				sync(i)
			end
			table_remove(self, i)
			break
		end
	end
end}

-- ':Cn' means the method returns a callable connection
function Me:Cn(method) -- I use 'event:Cn()' instead of 'event:Connect()' to distinguish it from roblox methods that return RBXScriptConnections
	self[#self + 1] = method
	return setmetatable({self, method}, CnMt)
end
-- ':Cns' means the method uses a connection list
function Me:Cns(cns, method) -- 'cns' is short for 'connections', and is my lightweight version of a Maid class
	self[#self + 1] = method
	cns[#cns + 1] = setmetatable({self, method}, CnMt)
end

function Mt:__call(...) -- I use 'event(...)' instead of 'event:Fire(...)'
	local i0, i1 = 0, #self
	
	-- I have other implementations that "lock" the list instead of creating functions (which is very expensive), but they're more complex.
	-- This implementation creates unneeded functions for 0 or 1 methods, but I'm leaving those out for the sake of simplicity.
	local syncPrev = self[0]
	self[0] = syncPrev and function(i) -- Supports recursive calling, as well as disconnecting arbitrary methods during other methods
		if i<=i1 then i1=i1-1 end
		if i<=i0 then i0=i0-1 end
		syncPrev(i)
	end or function(i) -- use fewer upvalues
		if i<=i1 then i1=i1-1 end
		if i<=i0 then i0=i0-1 end
	end
	
	-- Calling backwards is better for some cases, but calling in order of connection is more intuitive
	while i0 < i1 do
		i0 = i0 + 1
		-- This should never yield. If a method needs a coroutine, it should create one itself.
		-- This is not protected from errors, but I find this useful because they are often caused by a bad input.
		self[i0](...)
	end
	
	self[0] = syncPrev
end

function Class.new()
	return setmetatable({}, Mt)
end


do -- Usage:
	local event = Class.new()
	
	local cn = event:Cn(function(...)
		print("Event firing:", ...)
	end)
	
	event(1, 2, 3)
	cn()
	event(4, 5, 6)
end

return Class

@Tiffblocks has a simple implementation as well. I like it because disconnecting has low complexity (doesn’t require searching through a list for the method), but the pairs iterator can behave unexpectedly when connecting/disconnecting during the fire.

3 Likes