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.

13 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.

27 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

It may be possible with an type annotation method, but it is a bit risky (lag issues because while loop). there is one i used in my project:

local utility = {}
do
	function utility.connection(value : boolean, onDisconnect : boolean)
		local con : RBXScriptConnection = {}
		con.Connected = value
		function con:Disconnect()
			con.Connected = false
		end
		return con
	end
	
end

do
		local event : RBXScriptSignal = {}
		local funcs = nil
		local connection = utility.connection(true, function() funcs = nil end)
		
		
		function event:Connect(func : () -> ())
			funcs = func
			spawn(function()
				while wait() do
					if events.ShotEvent == 1 and funcs then
						events.ShotEvent = 0
						func()
					end
					if funcs == nil then break end
				end
			end)
		end
		
		function event:ConnectParallel(func : () -> ())
			funcs = func
			task.spawn(function()
				while wait() do
					if events.ShotEvent == 1 and funcs then
						events.ShotEvent = 0
						func()
					end
					if funcs == nil then break end
				end
			end)
		end
		
		function event:Once(func : () -> ())
			funcs = func
			spawn(function()
				while wait() do
					if events.ShotEvent == 1 and funcs then
						events.ShotEvent = 0
						func()
						break
					end
					if funcs == nil then break end
				end
			end)
		end
		
		function event:Wait()
			while wait() do
				if events.ShotEvent == 1 then
					events.ShotEvent = 0
					break
				end
			end
		end
		
		gun.OnGunshotFired = event
	end

If you want to add parameters into the connect or any functions inside the database of RBXScriptSignal, When first typing it wont show up, but when you entered the function it will show up no matter (sorry for bad english idk how to explain this)

Dude, that post was seven years ago, please don’t bump it again…

Anyways there’s sources like GoodSignal and Signa (By sleitnick)l that act exactly like a bindable event.

1 Like