A tip for all OOP coders out there!

Problem
When writing OOP modules we often want to add signals such as object.Changed, object.Completed etc. To write these you usually do:

local event = Instance.new("BindableEvent")
self.OnComplete = event.Event
self._onCompleteEvent = event

and in your code you fire it by doing

self._onCompleteEvent:Fire()

This gets very messy!

Solution
Instead add these two functions:

function Object:CreateEvent(name)
	local event = Instance.new("BindableEvent")
	self["_"..name] = event
	self[name] = event.Event
end

function Object:FireEvent(name, ...)
	self["_"..name]:Fire(...)
end

Example code:

self:CreateEvent("OnComplete") --Create the signal
self:FireEvent("OnComplete", 1, "2", true) --Fire the event
Object.OnComplete:connect(function(param1, param2, param3)
	print(param1, param2, param3) --prints: 1 2 true
end)

I found myself using this frequently, thought I’d share the practice.
Let me know if you find it useful!

8 Likes

Personally recommend building your own implementation of events if you are using it like this, That way you get around the annoyance of having store the bindable and the bindables event and makes it easier to clean stuff up from outside the class

2 Likes

Why not just build custom events at this point… e.g.

local function CreateEvent()
	local EventClass = {}

	function EventClass:Fire(...)
		return EventClass.Callback(...)
	end

	function EventClass:Connect(Callback)
		EventClass.Callback = Callback
	end

	return EventClass
end

local Event = CreateEvent()

Event:Connect(print)

Event:Fire("Hello world") -- Hello world


(this is also automatically garbage collected, like instances are when there are no references left attached to them)

2 Likes

Wow, I never thought of doing that

I reccomend to change EventClass.Callback to a table for multiple event listeners.

2 Likes

Your eventclass isn’t asynchronous and doesn’t support multiple listerners.

How would that look?
You are going to have to save either a bindable or a function that triggers the event. Eitherway there’s gonna be two variables stored in your class.

1 Like

-- Asyncronous version:

local function CreateEvent()
	local EventClass = {
		Callbacks = {}	
	}

	function EventClass:Fire(...)

		for Callback in pairs(self.Callbacks) do
			task.defer(Callback, ...)
		end
	end

	function EventClass:Connect(Callback)
		self.Callbacks[Callback] = true
  		local EventConnection = {}
		
		function EventConnection:Disconnect()
			self.Callbacks[Callback] = nil
		end
        return EventConnection
	end

	return EventClass
end

local Event = CreateEvent()

Event:Connect(print)

Event:Fire("Hello world") -- Hello world

An asyncronous and multiple listeners event…

(also automatically gets garbage collected)

sidenote: there is a much more efficient way to code this from a memory perspective, this is just to follow the standards people are used to with events, i personally tend to avoid using as many tables as this to hold functions and things.

2 Likes

Nice solution!
What would be the memory improving solution? Surely a few tables can’t be that bad?

1 Like

From the benchmarks I completed when testing my personal event class, if you can get the event down to just being a table containing the connected functions its ~2x more memory efficient then using bindables.

These custom event classes also allow more configuration (e.g. firing synchronously and asynchronously for performance, connecting a callback for when all connects to the event are removed so you can clean it up etc.)

You are going to have to save either a bindable or a function that triggers the event. Eitherway there’s gonna be two variables stored in your class.

I think Artoshia basically explained this, But yeah if you are just directly allowing connecting and firing the event then you are only storing one instance

1 Like

I think I’ll end up using @Artoshia solution, it’s very convinient to be able to do:
self.OnChanged = Event.new()
and then just call and connect to the same variable.

For anyone reading, here’s the modified version I’ll be using:

--Credits to Artoshia

local Event = {}
Event.__index = Event

function Event.new()
	local self = setmetatable({}, Event)
	self.Callbacks = {}	
	
	
	return self
end

function Event:Fire(...)

	for Callback in pairs(self.Callbacks) do
		task.defer(Callback, ...)
	end
end


function Event:Connect(Callback)
	local callbacks = self.Callbacks
	callbacks[Callback] = true
	
	local EventConnection = {}
	
	function EventConnection:Disconnect()
		callbacks[Callback] = nil
	end
	EventConnection.Destroy = EventConnection.Disconnect
	EventConnection.disconnect = EventConnection.Disconnect
	
	return EventConnection
end

Event.connect = Event.Connect

return Event

EDIT: Did small tweaks and fixed the disconnect bug.

More featureful implementations of custom signals can be found literally everywhere on the forum, for instance: Lua Signal Class Comparison & Optimal `GoodSignal` Class
Methods like DisconnectAll, or just the Wait method can be very useful.

1 Like