When should I be using custom signals?

I was looking at the NevermoreEngine’s Signals and Maids module, and I managed to figure out how to have custom signal events and fire them like so:

-- MODULE SCRIPT
local TestModule = {}
TestModule.__index = TestModule

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

	local maid = Maid.new()
	self._maid = maid
	self._exampleSignal = Signal.new()
	self._maid:GiveTask(self._exampleSignal:Connect(function(val1, val2, val3)
		CallFunctions:FireMySection1(val1,val2,val3)
	end))

	return self
end

function TestModule:fireSection(val1,val2,val3)
	self._exampleSignal:Fire(val1,val2,val3) 
end

--- MAIN SCRIPT
local TestModule = MyClass.new()
TestModule:fireSection(20,10,8)

As you can see here, the fireSection() fires my _exampleSignal event which then calls my CallFunction method.

This brings me to some questions:

  1. Is this the way to correctly use custom signals?
  2. Is there a higher level of custom signals that I can apply to my code?
  3. When should I be using them?
2 Likes

You can use Signals to help decouple your code.

That’s basically how you use Signals although

this is redundant typically you would just make the signal a public member and call

object.PublicSignal:Connect(args)
-- or
object.PublicSignal:Fire(args)
1 Like

Using custom signals is the time where you don’t want polling in your code.

2 Likes

Thank you for your answer, but I’m failing to see the usefulness of custom signals. Isn’t it a bit redundant to have a custom signal function when you can do the same thing by simply having a method? Something like this:

--- MY MODULE SCRIPT
function TestModule.new()
	local self = {}
	setmetatable(self, TestModule)
	return self
end

function TestModule:fireFunction()
    CallFunctions:FireMySection1(val1,val2,val3)
end

--- MAIN SCRIPT
local TestModule = MyClass.new()
TestModule:fireFunction(20,10,8)

If possible, can you give me a scenario where I would use custom signals over methods?

RemoteEvents and BindableEvents are signals in a sense, but they have additional overhead that is unnecessary. A custom signal class is much more efficient than say a bindable event if you’re not using the added features of the roblox classes. As they said events like this are great for organization. I adapted a signal class for my own use from someone named Swordphin:

--- Modified from @Swordphin123's Signal source.

local connection = {}
connection.__index = connection

function connection:Create()
    return setmetatable({
        connections = {},
        waiting = {}
    }, connection)
end

function connection:Connect(Listener)
    table.insert(self.connections, Listener)
    return Listener
end

function connection:Fire(...)
    if self.connections[1] then  
        for i = #self.connections, 1, -1 do
            local newThread = coroutine.create(self.connections[i])
            coroutine.resume(newThread, ...)
        end
    end 
    
    if self.waiting[1] then
        for i = #self.waiting, 1, -1 do
            coroutine.resume(self.waiting[i], ...)
            self.waiting[i] = nil
        end
    end
end

function connection:Wait()
    local thread = coroutine.running()
    table.insert(self.waiting, thread)
    return coroutine.yield()
end

function connection:Disconnect(Listener)
    for i = 1, #self.connections, 1 do
        if Listener == self.connections[i] then
            table.remove(self.connections, i)
        end
    end
end

function connection:Delete()
    for i = 1, #self.connections, 1 do
        self.connections[i] = nil
    end
    
    for i = 1, #self.waiting, 1 do
        self.waiting[i] = nil
    end
end

return connection

I use these to better organize my code and know when something happens in another script without depending on that script. For example making a generalized weapon hit class that fires with information about hits to any other script that might be listening (RaycastHitbox uses a custom signal class to get this hit information out to the script that created the hitbox). It’s useful in abstraction. Abstraction is a programming term that basically means you don’t need to know how it works, just that it does. It hides the unnecessary mess and only exposes a signal to listen to and while there may be thousands of lines of code behind it, the end user only needs to know 3 or 4: creating, and connecting/disconnecting. It’s a way of reducing complexity to the end user, or yourself.

5 Likes

What’s the difference between Signal class and Methods?

The point is that you can attach a callback function to an “action” which will run when said action happens.

Directly
local TestModule = {}
TestModule.__index = TestModule

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

    self._listeners = {}    

    return self
end

-- this allows us to pass a function that will run on "test"
function TestModule:ListenForTest(func)
     table.insert(self._listeners,func)
end
----------------------------------------------------------------
-- at this point we can fire the test listener whenever we want

function TestModule:Test(a,b,c)
    -- do whatever we want

    -- fire the listeners
    for _,listener in ipairs (self._listeners) do
       task.spawn(function()
           listener(a,b,c)
       end)
    end
end


return TestModule

Other Code:

local TestModule = require(TestModule)

local new_test = TestModule.new()

new_test:ListenForTest(function(a,b,c)
   -- this will fire when "Test" runs
   print(a,b,c)
end)

new_test:Test()
Signal

The Signal Class is just a way to abstract the same code from above

local Signal = require(Signal)

local TestModule = {}
TestModule.__index = TestModule

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

    self.TestChanged = Signal.new()
    return self
end


function TestModule:DoSomething()
    -- change some stuff

    self.TestChanged:Fire(thing_that_changed)
end

Other Code:

local TestModule = require(TestModule)

local new_test = TestModule.new()

new_test.TestChanged:Connect(function(test_that_changed)
     print(test_that_changed)
end)

new_test:DoSomething()

Signals don’t need to be used exclusively in objects


If I’m calling the change why do I need a Signal? I already know I am changing something.

This is the real question you are asking. The answer is to decouple your code.

The point of of Event-Driven-Programming is you can write code based on “Events” and that code is completely unrelated to the code that “fires” the event.

Example:

  • You want to give a player a sword when a player joins, you can use game.Players.PlayerAdded:Connect(func)

  • Your code “Sword giving code” is completely unrelated to the code that handles a player joining (that’s all backend roblox code)

  • Now you have your “sword giver” decoupled from roblox’s code

The more decoupled your code is, the easier it is find a particular set of code, the easier it is to make changes in your code, and the easier it is to find bugs in your code.

Because of that most games (and other applications) use event driven programming in order to react to changes without coupling code between systems.

You don’t need to go out looking for places to use a Signals, but if you ever see yourself needing to know when state changes (because another system changed) it. You can consider Signals.

I leave you with the Game Programming Patterns book,

Specifically these pages:

Architecture (this page talks about decoupling)
Observer Pattern (a more primitive verison of events, but it’s the same idea)

The rest of the book is also a gold mine especially if you plan on making a big game with a lot of programmers.

9 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.