[v.69] SimpleSignal | Very Simple & Silly Fast Script Signal

[(…)] SimpleSignal

SimpleSignal.rbxm (5.2 KB) | Donate 80 Robux

SimpleSignal is a module which I have used for everything for a while. From SimpleAnimate to SimpleComplete, they all use SimpleSignal. Though I never really bothered to publish it because there were already better signals on the market, and the only reason I kept it is because it was easy to maintain.

Until I decided to give a shot at optimizing it, and the results are actually pretty good, so this is getting its own post now!

Constructor

To create a new SimpleSignal object, you need to require the module and call the new function, like so:

local SimpleSignal = require(path.to.simplesignalmodule)

local signal = SimpleSignal.new()

And you’re done! You can also use SimpleSignal.RBXScriptSignal to use its types directly

Classes, Members and Methods

RBXScriptConnection

Member Name and Params (if is function) Return Type/Type Description
RBXScriptConnection:Disconnect() () Disconnects this RBXScriptConnection from its connected signal, so that it is never fired again.
RBXScriptConnection.Connected boolean Indicates wether or not this RBXScriptConnection is still connected to its signal.

RBXScriptSignal

Method Name And Params Returns Description
RBXScriptSignal:Connect(fn: (...: T...) -> ()) RBXScriptConnection Connects fn to be called with passed arguments when SimpleSignal:Fire(...) is called.
RBXScriptSignal:Fire(...: T...) () Calls all currently connected functions in the event in a seperate thread, passing ... as arguments.
RBXScriptSignal:Once(fn: (...: T...) -> ()) RBXScriptConnection Connects fn to be called with passed arguments when SimpleSignal:Fire(...) is called, but then fn is never called again. (fn is only fired once)
RBXScripSignal:Wait() T... Yields execution of the current thread until RBXScriptSignal:Fire(...) is called, upon which it resumes the current thread, returning the arguments passed by :Fire().
RBXScriptSignal:DisconnectAll() () Disconnects all currently connected RBXScriptSignals.
RBXScriptSignal:Destroy() () Clears all memory that this RBXScriptSignal occupies, clears the objects contents and sets the metatable to nil.

This is compared to GoodSignal, the standard signal module, for Connect and Fire:

Happy coding

15 Likes

That’s not true. There are people who don’t know what signal modules are. I’ve had multiple people ask me to clarify what my signal module really was, although it’s now described well with documentation.

Great, but GoodSignal is one of the slowest. Compare it against some of the best like LemonSignal, FastSignal and Signal+.

8 Likes

Alright ill make some docs then

Note that this version of SimpleSignal is indeed a rewrite, I made it today :V

3 Likes

The fire test is looking suspecious. There’s zero way SimpleSignal is 12x faster.
I’ll be back with a benchmark soon.

2 Likes

Test it urself

-- Benchmark utility module

local function table_sum(t)
	local sum = 0
	
	for _, v in t do
		sum += v
	end
	
	return sum
end

local function table_maxv(t)
	local max = -math.huge
	
	for _, v in t do
		if v > max then
			max = v
		end
	end
	
	return max
end

local function table_minv(t)
	local min = math.huge
	
	for _, v in t do
		if v < min then
			min = v
		end
	end
	
	return min
end

local function table_mode(t)
	local counts = {}
	local maxCount = 0
	local modeValue = 0

	for _, value in t do
		counts[value] = (counts[value] or 0) + 1
		if counts[value] > maxCount then
			maxCount = counts[value]
			modeValue = value
		end
	end

	return modeValue -- Return only the first most popular mode
end

local function dround(n: number, r: number)
	local p = 10^r
	
	return math.floor(n * p) / p
end

-- Returns the time it took <code>testfn</code> to run after the initial call.
local function benchmark_start<A..., R...>(testfn: (A...) -> R..., ...: A...): (number, R...)
	local start = os.clock()
	
	local result = {testfn(...)}
	
	return os.clock() - start, unpack(result)
end

--[[
	Calls <code>testfn</code> <code>repeats</code> times and returns <code>avtype</code> elapsed time (truncated to <code>decimals</code> decimal places).
]]
local function benchmark_findavg(repeats: number, avtype: "mean"|"max"|"min"|"median"|"mode"|"total", decimals: number, benchInterval: number, testfn: () -> ()): (number, {number})
	local times = {}
	local total = 0
	
	for i = 1, repeats do
		local t = benchmark_start(testfn)
		total += t
		
		table.insert(times, t)
		if i % benchInterval == 0 then
			task.wait()
		end
	end
	
	table.sort(times)
	if avtype == "max" then
		return dround(table_maxv(times), decimals), times
	elseif avtype == "min" then
		return dround(table_minv(times), decimals), times
	elseif avtype == "median" then
		return dround(times[#times//2], decimals), times
	elseif avtype == "mean" then
		return dround(table_sum(times)/repeats, decimals), times
	elseif avtype == "mode" then
		return dround(table_mode(times), decimals), times
	elseif avtype == "total" then
		return dround(total, decimals), times
	else
		error(`Unknown avtype {avtype}`)
	end
end

local function stdDeviation(times, mean)
	local sum = 0
	for _, v in times do
		sum += (v - mean)^2
	end
	return math.sqrt(sum/#times)
end

local Benchmark = {
	findavg = benchmark_findavg,
	start = benchmark_start,
	stdDeviation = stdDeviation,
	
	--[[<docstring>
		hi
	]]
	cool_var = 10
}

return Benchmark
4 Likes

Nevermind it’s possible that it’s ~12x faster at fire, but that’s simply because it creates the thread beforehand. SimpleSignal uses ~3x the memory, and is overall ~10x slower.

5 Likes

I benchmarked the memory, it really isn’t as bad as you say it is
Besides, imo runtime performance is more valuable than memory, so that’s why I think the tradeoff is pretty good :V

Btw I wanna say sorry for bashing you about SignalPlus, that was unprompted and likely came off as very rude :confused:

3 Likes

Fair enough. It’s an interesting way of doing it, which I haven’t seen done before. I’ll test it out with Signal+ and may or may not update it, if you don’t mind.

Thanks, it’s all good. :slight_smile:

3 Likes

Yeah I was actually wondering why no other signal module did this, it seemed like the most obvious solution :V

I don’t mind (just give me credit plsplspls :face_holding_back_tears: )

3 Likes

Alright the main post has some pretty simple docs now, glad broken spawn taught me how to format tables :slight_smile:

3 Likes

I can’t believe I’ve been missing out on such a cool markdown feature. How do I do those tables like you just added in the documentation?

3 Likes

It’s made like this:

| Column1 | Column2 | Column3 |
| --- | --- | --- |
| A | B | C|
| D | E| F |

This turns into this:

Column1 Column2 Column3
A B C
D E F
3 Likes

Package Version 43:

  • SimpleSignal is now distributed under a modified MIT license

  • Added error handling for event handlers, disconnecting all handlers if an error in one occurs

  • Removed the Connections field, opting to turn the entire RBXScriptSignal into what is essentially an array with a metatable

I’ve never heard of this? SimepleAnimate? SimpleComplete? What exactly would this primarily be used in/for? Is there some videos on youtube about it?

3 Likes

SimpleSignal is used in any usecase where you wanna have a signal, for example, in my module SimpleZone:

local zone = SimpleZone.new(...whateverparamsitdoesntmatterrn...)

zone:BindToHeartbeat()
zone.ItemEntered:Connect(function(item)
      print(item)
end)

ItemEntered is a Signal created by SimpleSignal, it’s just like the normal RBXScriptSignals roblox provides like game.DescendantAdded or BasePart.Touched, except you can completely customize it!

The difference with SimpleSignal is that it’s silly fast at firing events, most other modules take way too long to fire.
Also the fact that SimpleSignal is… well… simple. It’s only 162 lines + MIT license not including the Types module which is there purely for types

Any videos that teach you about stuff like GoodSignal (a module @stravant made) would teach you about SimpleSignal too, since both are the same thing in terms of function names and functionality

3 Likes

Ah interesting. Im surprised ive never heard of something like this. I didnt think there was a thing to modify the speeds of firing events. Do you know at all how this preforms on larger scale projects? I forgot where I saw the documentation but there was something similar to this but in larger scales its affects actually fell off like in an exponential graph.

2 Likes

There isn’t, the speed is an optimization :V

Well… SimpleZone is somewhat of a large-scale project, so is LessSimpleZone, infact the creator of Warp themselves uses the latter one!

Also, the time complexity for SimpleSignal:Fire() is veeery close to O(1) (in fact it might actually be O(1)) since it’s only looping through an array, and coroutine.resume() itself is O(1).

So for a large amount of connections, you can be assured SimpleSignal will still be quite performant :slight_smile:

4 Likes

Yeah Il’l give this one a shot and play around with it.

2 Likes

Yeah by the sounds of it I’ll do the same

Perfect thank you learning lots of new things!

1 Like