Engine-Native Signal Implementation

Signal… Oh Signal… Long ago in Roblox’s history, a faux-native way to create custom signals was possible via the RbxUtility library. The fact the library was somewhat lacking, along with the fact that the library literally no long exists, lead to a power vacuum which BindableEvents awkwardly sit next to. Now we have the single most reinvented wheel in roblox’s history.

As for BindableEvents . They have their own issues because they are generally less performant than pure luau implementations because they must carry the baggage of Instance.new-ing the BindableEvents. Typed RbxScriptSignals are also impossible, so many modern signal librarys allow for Signal.new() :: Signal<whatever> to make up for this. But also, the high demand for a Signal class should be enough evidence for BindableEvents being insufficient.

I feel like the implementation for a native Signal object should be fairly obvious, (a simple Signal.new() constructor) but it’s worth considering if they should match BindableEvent behavior of hiding Connect and Wait inside an Event property, or if Connect, Wait, and Fire should all be exposed on the same level like many modern signal implementations do.

18 Likes

https://create.roblox.com/store/asset/15239217296/Signal I guess this is close enough

To be honest I don’t think there is enough motivation for this especially when there are many Signal library out there. As for Bindable event, sure Roblox do really need to improve its performance. I mainly use bindable event for communicating between scripts especially parallel scripting.

2 Likes

We already have RBXScriptSignal that is used internally by the engine, so something delegated to game use natively could be an interesting concept.

However I don’t disagree with everyone else here; the solution was already made by the community and there wouldn’t be much of an incentive to use it over existing resources.

1 Like

tuff ping. native signals could be a cool task addition but its really not needed. Roblox’s solution is BindableEvents and it will prolly stay that way. Nothing really wrong with people making a bunch of signal modules either it’s a good way to learn the basics.

These are not really a solution since they have to pass through an instance first, it’d be way more ideal to have something that is just the signal object with none of the extra baggage (memory usage, etc…) that an instance has.

1 Like

yeah buddy i know that. I’m not saying it wouldn’t be good, im saying theres no motive for it. Instance overhead isn’t good but it’s not unusable for amateur scripters. It’s not like all signals are going for the same thing anyways, general purpose implementation would be cool but wouldn’t outshine custom made signals all-together
.

1 Like

Not needed
Iterate an array of functions and here is your signal but with no bloat

BindableEvent/Function serve a different purpose, to communicate cross sandbox or cross VM

Also dont like you supposed to make luau rfc for it rsther than posting it here?

Just because engine has it does not mean you should structure rest of code this way

This is just confusing and not aligned with the RBXScriptSignal syntax

Other game engines, such as Godot and Unity[1] set precedent for exposing methods of programatically creating Signal/Event objects

Not necessarily. An implementation could simply be exposing RbxScriptSignal as a creatable object.

local Broadcaster, Event = RbxScriptSignal.new()
local Connection = Event:Connect(function(...)
    return ...
end)
Broadcaster:Fire()

That being said, BindableEvents are notoriously slow. Now I don’t know if this is because it’s an instance or what. If it’s truly a problem of RbxScriptSignals then I guess, yes, a new engine type would be in order which would hopefully introduce some optimizations that we otherwise cannot do on our side of the C-barrier.

Maybe in some cases, but this doesn’t allow for Wait()ing without writing some rather annoying code.

local function waitForThis(callbackMap: {[(any) -> (any)]: boolean})
  local currentThread = coroutine.running()

  function callback(...)
    callbackMap[callback] = nil
    coroutine.resume(currentThread, ...)
  end
  callbackMap[callback] = true

  return coroutine.yield()
end

Now, rewriting this a few times across some scripts is fine I suppose, but the more observers that would rather Wait()[2] the more you end up wanting to expose this as a function of whatever library the callbackMap resides in, and the more modules that utilize the callbackMap-style structure, the more incentive (both in terms of readability and memory) there is to make a utility library for this data structure. This essentially created a DOP Signal.

Those instances have existed for far longer than script sandboxing or proper multithreading has existed[3], but at the same time I don’t think you realize you’re actually agreeing with me here. Those are Instances which should be used specifically when you need an Instance in the DataModel. This could be a separate userdata like CFrame or Color3.

No, I’m not proposing a new Luau signal primitive, nor am i proposing a createsignal function of the coroutine library.


  1. Signal — Godot Engine (stable) documentation in English
    Unity - Scripting API: UnityEvent
    ↩︎

  2. mind you, the ability to Wait() is important for avoiding getting “lost” in the stack ↩︎

  3. 2012 | Roblox API Reference ↩︎

3 Likes

Guessing this is closest, official signal module we’ll get from Roblox. I’m hoping we’ll get native implementation (not talking about instance based signals) as well.

Events and Signals are synonymous. BasePart.Touched is a RBXScriptSignal. Returning a tuple in this case is incredibly redundant…

… Additionally, why not call it something simpler like Signal?

So, then, the implementation of those systems only gave use to BindableEvents that it lacked prior, no?

Ok how do you capture those cross VM

what a catastrophe!

2 Likes

That code block was presenting a hypothetical in whichRbxScriptSignal was creatable. The tuple return is because RbxScriptSignals on their own do not have an exposed :Fire() method. So a separate object dedicated for firing its associated RbxScriptSignal would be necessary. A Signal class is perfectly reasonable if, in this scenario, the engine-native signal wasn’t a RbxScriptSignal.

Sorry if I wasn’t fully clear. I was trying to explain that the purpose of being able to create your own signals was for scenarios where you don’t care about cross-VM communication. userdata types already don’t play with parallel scripting.

1 Like

It would still be userdata, wouldn’t matter.

And that userdata wouldn’t have a lot of the overhead like higher memory usage that Instances have, so it would matter.

Signal would definitely inherit from either Object or Instance. If they don’t need to be parented, then most likely Object, which would still give some overhead most likely.

1 Like

I remember a discussion related to this in the past, but for a built-in solution for signals in Luau. The response was pretty much it would be less efficient to provide it that way versus someone making it in Luau. That being said it would still be nice to have a one solution and not need to choose between 1000+ implementations with slight differences. I use my own signal library. If the solution is provided by Roblox I suppose BindableEvent could be improved but I rarely use them.

1 Like

Why would it be an object? isn’t it more reasonable/likely it would be a data type just like RBXScriptSignal is (unless the docs are really confusing and it’s not actually a data type)?

That does not establish that Luau should have them.

Other engines having an abstraction is not evidence that the abstraction is appropriate for Luau. Luau is not C++, Godot, or Unity, and every piece of runtime machinery added to Luau has an implementation and execution cost.

If the same behavior can already be expressed with basic Luau primitives, adding another runtime abstraction needs an actual justification.

“Other engines do it” is not that justification.

Because BindableEvents are not designed to be a general-purpose “signal” abstraction.

They are useful for communication across VM/sandbox boundaries, which is a completely different problem.

I’ve already explained this multiple times. Replacing a primitive designed for cross-boundary communication with a new runtime signal system simply because someone wants signal-like syntax is backwards.

I don’t see why that requires a signal abstraction.

Iterate an array of functions.

That’s your signal.

for _, callback in callbacks do
    callback(...)
end

There is no need to “larp” a signal into existence with another layer of runtime machinery when the underlying operation is already trivial.

If there is some optimization that specifically requires a dedicated signal primitive, then explain what optimization it is and why it cannot be achieved with the existing primitives. (Real arguments only, no “trust the vibes”)

It does.

You can suspend the current coroutine and resume it later without introducing a signal abstraction.

For example:

task.delay(1, coroutine.running())
coroutine.yield()

The point is not that this exact snippet should replace every possible implementation. The point is that Wait() is not evidence that a dedicated signal object is necessary.

The code you provided is heavily structured around the concept of a “signal” rather than around the actual operation being performed.

That’s exactly the problem I’m criticizing.

Instead of asking:

“How do I implement this behavior?”

you’re asking:

“How do I make this behavior look like a signal?”

Those are not the same problem.

That is irrelevant to the argument.

The age of an API does not determine whether its current use case is appropriate.

What matters is what those instances are designed to do and what problem they are solving today.

Their historical existence does not establish that Luau needs another signal abstraction on top of them.

What are you referring to here?

I don’t understand what createsignal function you’re referring to in the coroutine library. If you’re referring to a specific API, please provide it, because there is no explanation here of what you mean.


At this point, I think the fundamental disagreement is pretty clear:

You are looking at signals as a useful abstraction that should exist because similar abstractions exist elsewhere.

I’m looking at the same problem and seeing an operation that can already be expressed directly with existing Luau primitives.

If the abstraction provides a concrete benefit that justifies its additional runtime machinery, demonstrate that benefit.

Until then, adding another layer simply because other engines have one is bloat.

No step back to the bloat. :raised_fist::gear:

1 Like