Signal
A Signal is a standalone Lua-side event object — call :Emit(...) to fire it, :Connect(node, func) to subscribe. Use it when you want an event channel that isn't tied to a specific Node's built-in signal table, e.g. a script-defined event you store as a field on a controller table.
⚠️ Polyphase has three different signal systems. They look similar, share the word "signal", and do not interoperate. Picking the wrong one is the most common cause of "I connected, I emitted, no error, but my callback never fires." Read the next section before writing new code.
The three signal flavors — pick one and stick with it
| System | API on the emitter | API on the listener | Lives on | Use it when |
|---|---|---|---|---|
| Node signals (built-in) | node:EmitSignal("name", { ... }) |
node:ConnectSignal("name", self, fn) |
A specific Node (auto-cleaned up when the node dies) |
One node owns the event — Button "Activated", Audio3D "OnFinished", Slider "ValueChanged", etc. Default choice. |
Signal objects (this page) |
mySignal:Emit(...) |
mySignal:Connect(self, fn) |
A Lua table you store anywhere — usually self.mySignal on a script |
You want an event channel that isn't a node, or your emitter isn't a node (e.g. a pure-Lua controller table) |
SignalBus |
SignalBus.Emit("name", ...) |
SignalBus.Subscribe("name", self, fn) |
Process-global, identified by string channel name | Cross-scene / cross-world communication, or the emitter doesn't know who's listening. See SignalBus.md. |
They do NOT cross-talk
Each system is its own channel — the name "OnFoo" in one system has zero connection to "OnFoo" in another. The most common mistake looks like this:
-- AudioPlaylist.lua — emitter side
function AudioPlaylist:Create()
self.OnNextSong = Signal:Create() -- ← creates a Lua Signal object
end
function AudioPlaylist:Next()
-- ... advance index ...
self.OnNextSong:Emit(self.currentIndex) -- ← emits on the Lua Signal object
end
-- AudioButtonBar.lua — listener side (BROKEN)
self.audioPlaylist:ConnectSignal("OnNextSong", self, function(self, idx)
self.songTitle:SetTextString(...)
end)
-- ConnectSignal subscribes to the NODE's built-in "OnNextSong" signal.
-- Nothing ever calls audioPlaylist:EmitSignal("OnNextSong", ...), so this
-- callback never fires. No error, no warning — silent.
How to fix it — match the systems
Pick one system on both ends.
Option A — use node signals everywhere (recommended; matches Activated, OnFinished, etc.):
-- emitter
function AudioPlaylist:Next()
-- ... advance index ...
self:EmitSignal("OnNextSong", { self:GetSongName() })
end
-- listener
self.audioPlaylist:ConnectSignal("OnNextSong", self, function(self, songName)
self.songTitle:SetTextString(songName)
end)
No Signal:Create() call needed — node signals are named on the fly and live on the node itself.
Option B — keep the Lua Signal and listen with :Connect:
-- emitter (same as before)
self.OnNextSong = Signal:Create()
-- ...
self.OnNextSong:Emit(self.currentIndex)
-- listener — go through the Signal object, NOT ConnectSignal
self.audioPlaylist.OnNextSong:Connect(self, function(self, idx)
self.songTitle:SetTextString(self.audioPlaylist:GetSongName())
end)
Quick decision tree
- I'm publishing an event from a Node that other nodes will listen to → Node signal (
node:EmitSignal/node:ConnectSignal). - I'm publishing from a non-node, or I want a signal that isn't named via a node's signal table →
Signal:Create()(this page). - I'm publishing something that crosses scene boundaries, or any subscriber may want it →
SignalBus.
When in doubt, prefer node signals — every example in the Polyphase docs (Activated on Button, OnFinished on Audio3D, ValueChanged on Slider) uses them, so call sites stay consistent.
API reference
A signal is an object that provides event-driven behavior. When a signal is emitted, any connected listeners react.
Create
Create a new signal.
Sig: signal = Signal.Create()
- Ret: Signal signal Newly created Signal
Emit
Emit the signal so that connected listeners can react. The listener function is called as func(listener, args...).
Sig: Signal:Emit(args...)
- Arg: args... Any number of arguments that will be passed to connected handler functions.
Connect
Connect a node to the signal. The function passed in should be a member function of the node's table — it will be invoked as func(node, args...) where args... are the values passed to Emit.
Example:
function HealthBar:OnHealthChanged(newHp)
self.quad:SetXRatio(newHp / Player.MaxHp)
end
function HealthBar:Start()
GetPlayer().healthSignal:Connect(self, HealthBar.OnHealthChanged)
end
Sig: Signal:Connect(listener, func)
- Arg: Node listener The node that will be notified when the signal is emit
- Arg: function func The function on the node that will be invoked when the signal is emit
### Disconnect
Sig: Signal:Disconnect(listener)
- Arg: Node listener The node that will be disconnected from the signal