Events, signals and tasks
Most scripts do two things: wait for something to happen, and schedule things to happen later. This page covers both: signals (events you attach functions to) and the task library (timers and threads).
How handlers run
Every event handler runs on its own thread. This applies to Touched, PlayerTouched, Clicked, MouseClick, GUI clicks, RunService.Frame, RunService.Draw, input, remotes, PlayerAdded, CharacterAdded, Died, tween Completed, Changed, ChildAdded and the rest. That means:
task.wait()andsignal:Wait()work inside any handler.- A handler that errors does not stop the other handlers of the same event.
- A handler that waits does not block the next firing of the event. If that matters, guard with a flag or wrap the function in
task.debounce. - Handlers of one signal run in the order they were connected.
Signals
A signal is an event object such as part.Touched or Players.PlayerAdded. You listen with :On.
| Member | Returns | Description |
|---|---|---|
signal:On(fn) | Connection | Calls fn every time the signal fires. |
signal:Once(fn) | Connection | Like On, but stops listening after the first firing. |
signal:Wait() | The signal's arguments | Yields until the signal fires. The thread is parked, not polled. |
connection:Off() | Stops calling the handler. | |
connection.Active | boolean | true until Off. |
local conn = part.Touched:On(function(hit)
print("touched by", hit.Name)
end)
task.wait(10)
conn:Off() -- stop listening after ten seconds
A signal accepts at most 500 handlers. Call Off on the ones you no longer need.
Events every object has
These exist on parts and other scene objects, the workspace, models and GUI elements.
| Event | Fires | Arguments |
|---|---|---|
inst.Changed | After a script assigns one of the object's properties. | The property name. On value objects: the new Value. |
inst:GetPropertyChangedSignal("Name") | After a script assigns that one property. | None. |
inst.ChildAdded | When a child is created, cloned, reparented in, or moved in. | The child. |
inst.ChildRemoved | When a child is destroyed or moved out. | The child. |
inst.DescendantAdded | When anything is added anywhere below the object. | The new object. |
inst.Destroying | Just before inst:Destroy() removes it. | None. |
ChildRemoved and Destroying run while the object still exists, so a handler can read its name and properties before it goes.
Changed only sees assignments made by scripts. Changes the engine makes itself, such as physics moving an unanchored part, do not fire it.
local lamp = workspace.Lamp
lamp:GetPropertyChangedSignal("Transparency"):On(function()
print("lamp is now", lamp.Transparency)
end)
workspace.ChildAdded:On(function(child) print("new object:", child.Name) end)
lamp.Destroying:On(function() print("lamp going away") end)
Touch and click events on parts
| Event | Arguments | Description |
|---|---|---|
part.Touched | hit | Fires on the server when something touches the part: a player's character part, or another part. A player fires it once on stepping onto the part. |
part.TouchEnded | hit | Fires once when the thing leaves. |
part.PlayerTouched | player, character, hit | Studz addition. Fires only for players, with the player and character already resolved. |
part.PlayerTouchEnded | player, character, hit | The matching end event. |
part.Clicked | player | Studz addition. The part's ClickDetector.MouseClick. The detector is created the first time you use Clicked. |
Prefer PlayerTouched over Touched when you only care about players. It saves you the hit.Parent:FindChild("Humanoid") and Players:FromCharacter dance:
-- The long way
part.Touched:On(function(hit)
local humanoid = hit.Parent:FindChild("Humanoid")
if not humanoid then return end
local player = Players:FromCharacter(hit.Parent)
-- ...
end)
-- Studz
part.PlayerTouched:On(function(player, character, hit)
-- ...
end)
The task library
| Function | Description |
|---|---|
task.wait([seconds]) | Yields the current script for seconds (default: one step). Returns the time actually waited. |
task.spawn(fn, ...) | Runs fn now on a new thread. If it yields with task.wait, it is rescheduled. |
task.after(seconds, fn, ...) | Runs fn on a new thread after seconds. (task.delay is the deprecated Roblox name.) |
task.defer(fn, ...) | Runs fn on the next step. |
task.every(seconds, fn, ...) | Calls fn again and again. Returns a handle with handle:Cancel(). An error inside fn is reported and the timer keeps running. |
task.debounce(fn, seconds) | Returns a wrapper that ignores calls arriving less than seconds after the last accepted one. |
task.cancel(thread) | Cancels a thread returned by spawn, after or defer. |
-- Repeat every 2 seconds, stop after 10 seconds
local timer = task.every(2, function() print("tick") end)
task.after(10, function() timer:Cancel() end)
-- Ignore button spam
local onClick = task.debounce(function(player)
player:Tell("Thanks!")
end, 1)
button.Clicked:On(onClick)
Frame events
RunService fires every frame. Get it with game:Service("RunService") or use the global.
| Member | Description |
|---|---|
RunService.Frame:On(function(dt) end) | Every simulation step. dt is seconds since the last one. |
RunService.Draw:On(function(dt) end) | Every rendered frame (client). |
RunService:IsServer() / :IsClient() | Opposites. IsClient is true on a player's machine. |
RunService:IsStudio() / :IsRunning() | Whether the script runs inside Polycarbonate, and whether the game is running. |
Anything in a frame event must finish within the 0.25 second time limit, so keep the work small and use task.every for slower periodic work.
Other event objects
BindableEvent signals between scripts on the same side (server to server, or client to client). Use :Fire(...) to send and .Event:On(fn) to receive. For server and client communication use remotes: see Client and server.
