Help with Metatables and oop

So I am trying to create a system right (and yes I did search before this, couldnt find like anything)
Anyhow what I want to do is be able to connect to a function in the oop or whatever you call it. For instance as in connecting, you can do this in roblox:

runservice.HeartBeat:Connect()

This allows you to connect that thing to a function, how can I do this with my own thing? I dont really have any code to show due to not knowing where to start whatsoever, thank you!

2 Likes
local RunService = {
	Hearbeat = {
		Connect = function(self, callback)
			while true do
				callback(task.wait())
			end
		end,
	}
}


RunService.Hearbeat:Connect(function(dt)
	print("hello world!", dt)
end)

Metatables is a way to enhance your normal table, make a table behave like a function or a string or a number etc

if you were to use the metamethod __call it would allow you to use your table as if it was a function.

local normalTable = {}
local metaTable = {__index = normalTable}

function metaTable:__call()
	print("we just called a table!")
end

setmetatable(normalTable, metaTable)

normalTable()

This is what happens without the metamethod __call()
image

This actually yields the current thread, meaning the connect function never returns. It is important to note that to achieve the same thing as what is described in the provided line of code, you will not only. need object-oriented programming with metatables, but also an implementation of signals that mimics the RBXScriptSignal interface. You can do this through wrapping bindable events, or you can use a library such as GoodSignal, which is what most people use and is what I recommend you use.

local RunService = {
	Hearbeat = {
		Connect = function(self, callback)
			local thread = task.spawn(function()
				while true do
					callback(task.wait())
				end
			end)
			return {
				Disconnect = function()
					task.cancel(thread)
				end,
			}
		end,
		
		Once = function(self, callback)
			task.spawn(callback, task.wait())
		end,

	}
}


local connection = RunService.Hearbeat:Connect(function(slow_dt)
	print("hello world!", slow_dt)
end)

print("this does not yield")

task.delay(0.5, function()
	connection:Disconnect()
end)

closure over OOP cause we don’t need metamethods.

-- This is as fast and efficient as GoodSignal, but lack the capacity to connect multiple listeners.
-- however, if you were to turn thread into a table and use table.insert in the connect and an ipairs loop in the fire callback it would still equal to GoodSignal (just not in the long run i guess? I still don't understand why the use of `freeRunnerThread` but I'm still researching about it)
-- Not sure why GoodSignal is obsessed with not memory leaking the arguments yet (I'm still researching about it) 

-- metatables for OOP is personal choice. metatable does not make OOP neither OOP makes metatable. You pick your poison based on convenience and readability.

local function makeSignal()
	local signal = {}
	
	local thread = nil
	local co = nil
	
	function signal:Fire(...)
		if thread then
			task.spawn(thread, ...)
			if co then
				task.spawn(co, ...); co = nil
			end
		end
	end
	
	function signal:Connect(callback)
		thread = callback
	end
	
	function signal:Wait()
		co = coroutine.running()
		return coroutine.yield()
	end
	
	function signal:Disconnect()
		thread = nil
	end
	
	return signal
end

local signal = makeSignal()
1 Like

This only works for one connection. Additionally it does seem counter productive to go out of your way to implement something that there are already many existing libraries for. If you were interested in making your own implementation, I recommend at least studying the code of these existing libraries and understanding the rationale behind their different design choices, so you can make something equivalent, or even better. The article on GoodSignal that I linked earlier includes comparisons with other signal implementations.

I think you’re confused. I’ve been in this platform for nearly 8 years, I use GoodSignal, I have almost all open source libraries in possession, I understand very well what you’re on about, but you see, I’m a person that likes to stay in topic, if OP asks for something I reply with exactly what he wants, I don’t consider x better than y because it’s not what OP wants to hear about.

About the signal only working for one listener, that is already stated in the code itself as a comment in the very first lines and is designed on purpose as I don’t like having one signal for many connections, neither I like to have metatables when I’m not using metamethods.

You assumed that I do that on a daily basis when I don’t. I don’t reinvent code, I research everyday, hence my presence in the devforum.

These replies of mine are simply byproduct of the things I’ve seen. Just think about it. How would I write a signal that has nearly identical behavior to GoodSignal if I had never seen it before?

Have you considered looking into Roblox’s own personal Signal? I wonder why they are not using GoodSignal, well probably because it’s not absolute neither definitive, but another way to make signals.

This is Roblox Signal by the way:

--!strict
local types = require(script.Parent.types)

type Callback<T> = types.Callback<T>
type Subscription = types.Subscription
type Signal<T> = types.Signal<T>
type FireSignal<T> = types.FireSignal<T>

type InternalSubscription<T> = { callback: Callback<T>, unsubscribed: boolean }

local function createSignal<T>(): (Signal<T>, FireSignal<T>)
	local subscriptions: { [Callback<T>]: InternalSubscription<T> } = {}
	local suspendedSubscriptions = {}

	local firing = false

	local function subscribe(_self: Signal<T>, callback)
		local subscription = {
			callback = callback,
			unsubscribed = false,
		}

		-- If the callback is already registered, don't add to the
		-- suspendedConnection. Otherwise, this will disable the existing one.
		if firing and not subscriptions[callback] then
			suspendedSubscriptions[callback] = subscription
		else
			subscriptions[callback] = subscription
		end

		local function unsubscribe(_self: Subscription)
			subscription.unsubscribed = true
			subscriptions[callback] = nil
			suspendedSubscriptions[callback] = nil
		end

		return {
			unsubscribe = unsubscribe,
		}
	end

	local function fire(value: T)
		firing = true
		for callback, subscription in subscriptions do
			if not subscription.unsubscribed and not suspendedSubscriptions[callback] then
				callback(value)
			end
		end

		firing = false
		for callback, subscription in suspendedSubscriptions do
			subscriptions[callback] = subscription
		end
		table.clear(suspendedSubscriptions)
	end

	return {
		subscribe = subscribe,
	}, fire
end

return createSignal

RobloxSignal.rbxm (4.8 KB)
FastCastSignal.lua (5.0 KB)

1 Like

@OP:
Do you mean you want to have a property of the object that’s an event, so you can do something like:

local myGameObject = require(script.MyGameObject).new()
myGameObject.GameOver:Connect(function()
    print("Game over!")
end)

If so, you can create a new BindableEvent then set the self.GameOver property (or which ever name you want) to the BindableEvent.Event.

You can then trigger the event with self.StoredBindableEvent:Fire().

TL;DR:

  • Create a BindableEvent inside the new/initialization function
  • Set the property name for the event to BindableEvent.Event
  • Also store the BindableEvent inside the object so you can trigger it later with :Fire()

(You can also use libraries that simulate Roblox events, such as the Signal or FastSignal libraries.)

1 Like