Can you make a custom 'Event' function on roblox?

I’ve seen in some scripts like ZonePlus, or Cast, that they have modules which will connect an Event I’ve never even heard of and it looks to be custom for when you do things such as walk into the zone.

I believe they are not events and that they are functions inside of the module. They are ran like that.

Well, technically you can, with a signal module.
Basically it’s a lua implementation of a remote event.

Example of a signal module that is pretty popular:

-- -----------------------------------------------------------------------------
--               Batched Yield-Safe Signal Implementation                     --
-- This is a Signal class which has effectively identical behavior to a       --
-- normal RBXScriptSignal, with the only difference being a couple extra      --
-- stack frames at the bottom of the stack trace when an error is thrown.     --
-- This implementation caches runner coroutines, so the ability to yield in   --
-- the signal handlers comes at minimal extra cost over a naive signal        --
-- implementation that either always or never spawns a thread.                --
--                                                                            --
-- API:                                                                       --
--   local Signal = require(THIS MODULE)                                      --
--   local sig = Signal.new()                                                 --
--   local connection = sig:Connect(function(arg1, arg2, ...) ... end)        --
--   sig:Fire(arg1, arg2, ...)                                                --
--   connection:Disconnect()                                                  --
--   sig:DisconnectAll()                                                      --
--   local arg1, arg2, ... = sig:Wait()                                       --
--                                                                            --
-- Licence:                                                                   --
--   Licenced under the MIT licence.                                          --
--                                                                            --
-- Authors:                                                                   --
--   stravant - July 31st, 2021 - Created the file.                           --
--   sleitnick - August 3rd, 2021 - Modified for Knit.                        --
-- -----------------------------------------------------------------------------

-- The currently idle thread to run the next handler on
local freeRunnerThread = nil

-- Function which acquires the currently idle handler runner thread, runs the
-- function fn on it, and then releases the thread, returning it to being the
-- currently idle one.
-- If there was a currently idle runner thread already, that's okay, that old
-- one will just get thrown and eventually GCed.
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
	local acquiredRunnerThread = freeRunnerThread
	freeRunnerThread = nil
	fn(...)
	-- The handler finished running, this runner thread is free again.
	freeRunnerThread = acquiredRunnerThread
end

-- Coroutine runner that we create coroutines of. The coroutine can be 
-- repeatedly resumed with functions to run followed by the argument to run
-- them with.
local function runEventHandlerInFreeThread(...)
	acquireRunnerThreadAndCallEventHandler(...)
	while true do
		acquireRunnerThreadAndCallEventHandler(coroutine.yield())
	end
end


-- Connection class
local Connection = {}
Connection.__index = Connection


function Connection.new(signal, fn)
	return setmetatable({
		_connected = true,
		_signal = signal,
		_fn = fn,
		_next = false,
	}, Connection)
end


function Connection:Disconnect()
	if not self._connected then return end
	self._connected = false

	-- Unhook the node, but DON'T clear it. That way any fire calls that are
	-- currently sitting on this node will be able to iterate forwards off of
	-- it, but any subsequent fire calls will not hit it, and it will be GCed
	-- when no more fire calls are sitting on it.
	if self._signal._handlerListHead == self then
		self._signal._handlerListHead = self._next
	else
		local prev = self._signal._handlerListHead
		while prev and prev._next ~= self do
			prev = prev._next
		end
		if prev then
			prev._next = self._next
		end
	end
end

Connection.Destroy = Connection.Disconnect

-- Make Connection strict
setmetatable(Connection, {
	__index = function(_tb, key)
		error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(_tb, key, _value)
		error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end
})


--[=[
	@class Signal
	Signals allow events to be dispatched and handled.
	For example:
	```lua
	local signal = Signal.new()
	signal:Connect(function(msg)
		print("Got message:", msg)
	end)
	signal:Fire("Hello world!")
	```
]=]
local Signal = {}
Signal.__index = Signal

--[=[
	Constructs a new Signal
	@return Signal
]=]
function Signal.new()
	local self = setmetatable({
		_handlerListHead = false,
		_proxyHandler = nil,
	}, Signal)
	return self
end


--[=[
	Constructs a new Signal that wraps around an RBXScriptSignal.
	@param rbxScriptSignal RBXScriptSignal -- Existing RBXScriptSignal to wrap
	@return Signal
	For example:
	```lua
	local signal = Signal.Wrap(workspace.ChildAdded)
	signal:Connect(function(part) print(part.Name .. " added") end)
	Instance.new("Part").Parent = workspace
	```
]=]
function Signal.Wrap(rbxScriptSignal)
	assert(typeof(rbxScriptSignal) == "RBXScriptSignal", "Argument #1 to Signal.Wrap must be a RBXScriptSignal; got " .. typeof(rbxScriptSignal))
	local signal = Signal.new()
	signal._proxyHandler = rbxScriptSignal:Connect(function(...)
		signal:Fire(...)
	end)
	return signal
end


--[=[
	Checks if the given object is a Signal.
	@param obj any -- Object to check
	@return boolean -- `true` if the object is a Signal.
]=]
function Signal.Is(obj)
	return type(obj) == "table" and getmetatable(obj) == Signal
end


--[=[
	Connects a function to the signal, which will be called anytime the signal is fired.
	@param fn (...any) -> nil
	@return Connection -- A connection to the signal
]=]
function Signal:Connect(fn)
	local connection = Connection.new(self, fn)
	if self._handlerListHead then
		connection._next = self._handlerListHead
		self._handlerListHead = connection
	else
		self._handlerListHead = connection
	end
	return connection
end


function Signal:GetConnections()
	local items = {}
	local item = self._handlerListHead
	while item do
		table.insert(items, item)
		item = item._next
	end
	return items
end


-- Disconnect all handlers. Since we use a linked list it suffices to clear the
-- reference to the head handler.
--[=[
	Disconnects all connections from the signal.
]=]
function Signal:DisconnectAll()
	self._handlerListHead = false
end


-- Signal:Fire(...) implemented by running the handler functions on the
-- coRunnerThread, and any time the resulting thread yielded without returning
-- to us, that means that it yielded to the Roblox scheduler and has been taken
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
--[=[
	Fire the signal, which will call all of the connected functions with the given arguments.
	@param ... any -- Arguments to pass to the connected functions
]=]
function Signal:Fire(...)
	local item = self._handlerListHead
	while item do
		if item._connected then
			if not freeRunnerThread then
				freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
			end
			task.spawn(freeRunnerThread, item._fn, ...)
		end
		item = item._next
	end
end


--[=[
	Same as `Fire`, but uses `task.defer` internally & doesn't take advantage of thread reuse.
	@param ... any -- Arguments to pass to the connected functions
]=]
function Signal:FireDeferred(...)
	local item = self._handlerListHead
	while item do
		task.defer(item._fn, ...)
		item = item._next
	end
end


--[=[
	Yields the current thread until the signal is fired, and returns the arguments fired from the signal.
	@return ... any -- Arguments passed to the signal when it was fired
	@yields
]=]
function Signal:Wait()
	local waitingCoroutine = coroutine.running()
	local cn
	cn = self:Connect(function(...)
		cn:Disconnect()
		task.spawn(waitingCoroutine, ...)
	end)
	return coroutine.yield()
end


--[=[
	Cleans up the signal.
]=]
function Signal:Destroy()
	self:DisconnectAll()
	local proxyHandler = rawget(self, "_proxyHandler")
	if proxyHandler then
		proxyHandler:Disconnect()
	end
end


-- Make signal strict
setmetatable(Signal, {
	__index = function(_tb, key)
		error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(_tb, key, _value)
		error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end
})

return Signal

Do you mean BindableEvents? they are custom events which can be used for cross script communication. Basically something like this:

local Event = Instance.new("BindableEvent", script) 

Event.Event:Connect(function(...)
	print(...)
end)

Event:Fire("hello!") 
1 Like

Do you know any tutorials on how to do this with signal? I’d like to know if possible, thanks!

Sorry for the late response.

Basics of signal
-- a module script can hold these, so every script can have access to it
local signal = require(game.ReplicatedStorage.Signal) -- change this where your signal module is

local myEvents = {}

myEvents.myCustomEvent = signal.new()

return myEvents

-- then a different module can do
local myEventsModule = require(nil) -- replace this where the module containing all of the signals are
myEventsModule.myCustomEvent:Fire("The chosen number is 5")

-- another script can listen to this custom event now
local myEventsModule = require(nil) -- replace this where the module containing all of the signals are
myEventsModule.myCustomEvent:Connect(function(text)
	warn(text)
end)
Disconnect from signals

You can also disconnect from signals

local myConnection = myEventsModule.myCustomEvent:Connect(function(arguments)
     print("Hi!")
end)
myConnection:Disconnect()

-- disconnects from every single connection that other scripts made
myConnection:DisconnectAll()
  • Then there is deferred fire, what is basically fire, but uses task.defer internally
  • Then signal:Wait(), that makes the thread yield (the script that called this function), until the signal gets fired, and returns the arguments from the fire method.
  • And finally destroy, that as the name suggests, destroys the class.

I’m 99% sure, that I did not miss out anything. Hope this helped!

4 Likes