Scripting on Roblox is primarily event-driven. The engine supports multiple types of events. When implementing your logic, you can connect functions to built-in events fired by the engine to respond to them. You can also create custom events that you fire and respond to. Additionaly, you can use networking events that allow event-driven communication across the client-server boundary.
Built-in Events
Many objects have built-in events provided by their APIs that automatically respond to specific actions or changes related to those objects. For example, a Player.Character touching a Part automatically fires a BasePart.Touched event. Most built-in events are synchronous, so you can connect a function for following custom behaviors in response a certain built-in event by scripting.
Connecting Functions to Events
You can connect a function to an event using Connect() to execute code each time the event fires. Most events pass arguments to their connected functions when they fire. For example, the BasePart.Touched event passes the object that touched the Part, and the Players.PlayerAdded event passes the Player that joined your experience.
It's the best practice to name the function with the pattern onEventName to help you find the function in the future. The following code sample demonstrates how to connect a function named onPartTouched to the BasePart.Touched event of a Part in the Workspace.
-- Script in ServerScriptService
local part = workspace.Part
local function onPartTouched(object)
print("Part was touched by", object:GetFullName())
end
part.Touched:Connect(onPartTouched)
You can connect anonymous functions to events when you want to use variables in the parent scope and don't need to use the function elsewhere. The following code sample shows how to connect an anonymous function to the Players.PlayerAdded event.
-- Script in ServerScriptService
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(player)
print(player.Name, " joined the game!")
end)
Disconnecting Functions from Events
In Luau, the RBXScriptSignal data type represents events. The RBXScriptSignal:Connect() method returns an RBXScriptConnection object. If you connect a function to an event but don't want the function to call in subsequent event firings, disconnect the function from the event by calling the RBXScriptConnection:Disconnect() method.
When Luau destroys an event's object, such as the Player object when a user leaves the experience, all of its connections disconnect automatically. The following code sample shows how to connect and disconnect a function to the Part.Touched event.
-- Script in ServerScriptService
local part = workspace.Part
local targetPart = workspace.TargetPart
-- Declare the variable before defining the function so it can be disconnected from within the function
local connection
local function onPartTouched(otherPart)
if otherPart == targetPart then
print("The part hit the target")
connection:Disconnect()
end
end
connection = part.Touched:Connect(onPartTouched)
Waiting for Events to Fire
If you want a script to yield or pause until a specific event fires, use the Wait function.
-- Script in ServerScriptServicelocal part = workspace.Partlocal touchedPart = part.Touched:Wait()print("The part was touched by", touchedPart:GetFullName())
The Wait function returns the event's arguments. You can assign these arguments to variables where Wait is called.
-- Script in ServerScriptServicelocal VirtualInputManager = game:GetService("VirtualInputManager")local isPressed, keyCode, isRepeatedKey, layerCollector = VirtualInputManager.SendKeyEvent:Wait()print(isPressed)print(keyCode)--..etc
Custom Events
Custom events allow you to bind behaviors between scripts and communicate your specific desired outcome for certain in-experience actions. Custom events can be both asynchronous and synchronous. and they can only communicate scripts within the same side of the client-server model.
Custom Asynchronous Events
BindableEvent allows asynchronous, one-way communication between scripts. You can use it to define a custom event and fire it manually by calling its BindableEvent:Fire() method without yielding for return values. The connected function receives arguments that you pass to Fire.
Creating Custom Asynchronous Events
To create a new BindableEvent in the Explorer:
- Hover over the container in the Explorer into which you want to insert a BindableEvent. It's common to put BindableEvents in an Event folder in ServerScriptService to use them with Scripts and ReplicatedStorage to use them with LocalScripts.
- Click the ⊕ button that appears to the right of the container to open the Insert Object menu.
- Select BindableEvent.
- Rename the event to describe its purpose using PascalCase.
To create a new BindableEvent in a Script, use Instance.new():
-- Script in ServerScriptServicelocal roundStartedEvent = Instance.new("BindableEvent")roundStartedEvent.Name = RoundStartedroundStartedEvent.Parent = ServerScriptService
Using Custom Asynchronous Events
You can connect multiple functions to the same BindableEvent, but Luau executes them in an unpredictable order. To ensure that functions execute in a particular order, combine their bodies or calls into a single function to connect to the event.
To fire a BindableEvent in ServerScriptService named ShareData:
-- Script in ServerScriptService named DataSenderlocal ServerScriptService = game:GetService("ServerScriptService")local shareData = ServerScriptService.ShareDatalocal HELLO_WORLD = "Hello world!"-- Shares HELLO_WORLD after 2 secondswait(2)shareData:Fire(HELLO_WORLD)
To connect to a BindableEvent in ServerScriptService named ShareData:
-- Script in ServerScriptService named DataReceiver
local ServerScriptService = game:GetService("ServerScriptService")
local shareData = ServerScriptService.ShareData
shareData.Event:Connect(function(data)
print(data)
end)
Custom Synchronous Events
BindableFunction objects allow for synchronous, two-way communication between scripts. They contain an OnInvoke callback that you can define in one script and call from other scripts. The callback function runs when you call the Invoke() method on the BindableFunction. The callback function arguments that you pass to Invoke(). The code invoking the function yields until the function halts or returns a value.
Creating Custom Synchronous Events
To create a new BindableFunction in the Explorer:
- Hover over the container in the Explorer into which you want to insert a BindableFunction. It's common to put BindableFunction objects in a Functions folder in ServerScriptService to use them with Scripts and ReplicatedStorage to use them with LocalScripts.
- Click the ⊕ button that appears to the right of the container to open the Insert Object menu.
- Select BindableFunction.
- Rename the function to describe its purpose using PascalCase.
To create a new BindableFunction in a Script:
-- Script in ServerScriptServicelocal getHelloWorld = Instance.new("BindableFunction")getHelloWorld.Name = GetHelloWorldgetHelloWorld.Parent = ServerScriptService
Using Custom Synchronous Events
Each BindableFunction has only one OnInvoke callback. If you have multiple definitions, then only the one defined latest runs. If the OnInvoke callback does not have a return statement, it returns nil.
To define the OnInvoke callback of a BindableFunction in ServerScriptService named GetHelloWorld:
-- Script in ServerScriptService named DataManager
local ServerScriptService = game:GetService("ServerScriptService")
local getHelloWorld = ServerScriptService.GetHelloWorld
local HELLO_WORLD = "Hello world!"
getHelloWorld.OnInvoke = function()
return HELLO_WORLD
end
To invoke a BindableFunction in ServerScriptService named GetHelloWorld:
-- Script in ServerScriptService named DataReceiverlocal ServerScriptService = game:GetService("ServerScriptService")local getHelloWorld = ServerScriptService.GetHelloWorldprint(getHelloWorld:Invoke())print("Hello world again!")
Notice that the "Hello world!" prints before "Hello world again!" because the Script yields until the BindableFunction halts or returns a value.
Networking Events
All experiences require communication between the server and the players' clients. For example when a player presses W to move forward, the keyboard input is received on the player's client. The client then communicates this to the server, which responds by moving the position of the player's character forward. RemoteEvent and RemoteFunction objects allow you to create your own events and functions to communicate your own behavior between the client-server boundary.
RemoteEvents allow for asynchronous, one-way communication across the boundary, while RemoteFunctions allow for synchronous, two-way communication (sending a request across the boundary and yielding until a response is received from the recipient).
The following table serves as a quick reference for how to use remote events and functions to communicate between the client and server.
Remote Events
Client → ServerClient | RemoteEvent:FireServer(args) |
Server | RemoteEvent.OnServerEvent:Connect(function(player, args)) |
Server | RemoteEvent:FireClient(player, args) |
Client | RemoteEvent.OnClientEvent:Connect(function(args)) |
Server | RemoteEvent:FireAllClients(args) |
Client | RemoteEvent.OnClientEvent:Connect(function(args)) |
Remote Functions
Client → Server → ClientClient | serverResponse = RemoteFunction:InvokeServer(args) |
Server | RemoteFunction.OnServerInvoke = function(player, args) |
Asynchronous Networking Events
A RemoteEvent object allows asynchronous, one-way communication across the client-server boundary. You can use it to send requests across the boundary without yielding for a response.
Creating Asynchronous Networking Events
Create RemoteEvent objects in a location that both the client and server can access, such as ReplicatedStorage.
In the Explorer window, hover over ReplicatedStorage and click the ⊕ button.
Click RemoteEvent from the contextual menu.
Rename the new RemoteEvent to describe its purpose.
Using Asynchronous Networking Events
You can use RemoteEvents to facilitate one-way communication from one client to the server, the server to one client, or the server to all clients.
Client to Server
You can use a LocalScript to trigger an event on the server by calling the FireServer() method on a RemoteEvent. If you pass arguments to FireServer(), they pass to the event handler on the server. The first parameter of the event handler is always the Player object of the client that calls it, and additional parameters follow.
Client | RemoteEvent:FireServer(args) |
Server | RemoteEvent.OnServerEvent:Connect(function(player, args)) |
The following LocalScript and Script fire and respond to a RemoteEvent instance in ReplicatedStorage named RemoteEventTest. The LocalScript calls FireServer() on the RemoteEvent instance. In response, the Script connects an event handler to OnServerEvent that creates a new Part on the server.
LocalScript in StarterPlayerScripts
local ReplicatedStorage = game:GetService("ReplicatedStorage")local remoteEvent = ReplicatedStorage:WaitForChild("RemoteEventTest")-- Fire the RemoteEvent and pass additional argumentsremoteEvent:FireServer(BrickColor.Red(), Vector3.new(0, 25, 0))
Script in ServerScriptService
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteEvent = ReplicatedStorage:WaitForChild("RemoteEventTest")
local function onCreatePart(player, partColor, partPosition)
print(player.Name .. " fired the RemoteEvent")
local newPart = Instance.new("Part")
newPart.BrickColor = partColor
newPart.Position = partPosition
newPart.Parent = workspace
end
remoteEvent.OnServerEvent:Connect(onCreatePart)
Server to Client
You can use a Script to trigger an event on a client by calling the FireClient() method on a RemoteEvent. The first argument of the FireClient() is the Player object of the client that you want to respond to the event, and additional arguments pass to the client.
Server | RemoteEvent:FireClient(player, args) |
Client | RemoteEvent.OnClientEvent:Connect(function(args)) |
The following Script and LocalScript fire and respond to a RemoteEvent instance in ReplicatedStorage named RemoteEventTest. The Script calls FireClient() on the RemoteEvent to fire it. The LocalScript connects an event handler to the OnClientEvent event. The event handler doesn't need to list the Player object as its first argument because you can determine the player on the client with Players.LocalPlayer.
Script in ServerScriptService
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local remoteEvent = ReplicatedStorage:WaitForChild("RemoteEventTest")
local function onPlayerAdded(player)
print("[Server] Firing event to player", player.Name)
remoteEvent:FireClient(player, Players.MaxPlayers, Players.RespawnTime)
end
Players.PlayerAdded:Connect(onPlayerAdded)
LocalScript in StarterPlayerScripts
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local remoteEvent = ReplicatedStorage:WaitForChild("RemoteEventTest")
local player = Players.LocalPlayer
local function onNotifyPlayer(maxPlayers, respawnTime)
print("[Client] Event received by player", player.Name)
print(maxPlayers, respawnTime)
end
remoteEvent.OnClientEvent:Connect(onNotifyPlayer)
Server to All Clients
You can use a Script to trigger an event on all clients by calling the FireAllClients() method on a RemoteEvent. Unlike FireClient(), the FireAllClients() method doesn't require a Player object because it fires the RemoteEvent to all clients.
Server | RemoteEvent:FireAllClients(args) |
Client | RemoteEvent.OnClientEvent:Connect(function(args)) |
The following Script and LocalScript fire and respond to a RemoteEvent instance in ReplicatedStorage named RemoteEventTest. The Script calls FireAllClients() on the RemoteEvent to fire it. The LocalScript connects an event handler to the OnClientEvent event that prints to the Output window.
Script in ServerScriptService
local ReplicatedStorage = game:GetService("ReplicatedStorage")local remoteEvent = ReplicatedStorage:WaitForChild("RemoteEventTest")local secondsRemaining = 5-- Fire the RemoteEvent every second until time expiresfor t = secondsRemaining, 1, -1 doremoteEvent:FireAllClients(t)task.wait(1)end
LocalScript in StarterPlayerScripts
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteEvent = ReplicatedStorage:WaitForChild("RemoteEventTest")
local function onTimerUpdate(seconds)
print(seconds)
end
-- Call "onTimerUpdate()" when the server fires the RemoteEvent
remoteEvent.OnClientEvent:Connect(onTimerUpdate)
Client to Client
Clients cannot communicate directly with other clients, although you can effectively dispatch an event from one client to another by using the FireServer() method, then calling FireClient() or FireAllClients() in the event handler for OnServerEvent.
Synchronous Networking Events
RemoteFunction allows synchronous, two-way communication across the client-server boundary. The sender of a remote function will wait to receive a response from the recipient.
Creating Synchronous Networking Events
Create RemoteFunction objects in a location that both the client and server can access, such as ReplicatedStorage.
In the Explorer window, hover over ReplicatedStorage and click the ⊕ button.
Click RemoteFunction from the contextual menu.
Rename the new RemoteFunction to describe its purpose.
Using Synchronous Networking Events
You can use RemoteFunction objects to facilitate two-way communication between one client and the server or the server and one client.
Client to Server to Client
You can use a Script to call a function on the server by calling the InvokeServer() method on a RemoteFunction. The LocalScript that invokes the RemoteFunction yields until the callback of the RemoteFunction returns. Arguments that you pass to InvokeServer() pass to the OnServerInvoke callback of the RemoteFunction. If you define multiple callbacks to the same RemoteFunction, the last definition executes.
Client | RemoteFunction:InvokeServer(args) |
Server | RemoteFunction.OnServerInvoke = function(player, args) |
The following LocalScript and Script have two-way communication using a RemoteFunction instance in ReplicatedStorage named RemoteFunctionTest. The LocalScript calls InvokeServer() on the RemoteFunction instance with extra arguments. In response, the Script defines the callback of the RemoteFunction and calls it using the Player of the client and the extra arguments. The Script then returns the return value of the callback to the LocalScript.
LocalScript in StarterPlayerScripts
local ReplicatedStorage = game:GetService("ReplicatedStorage")local remoteFunction = ReplicatedStorage:WaitForChild("RemoteFunctionTest")-- Invoke the Function-- Pass a brick color and position when invoking the functionlocal newPart = remoteFunction:InvokeServer(BrickColor.Red(), Vector3.new(0, 25, 0))print("The server created the requested part:", newPart)
Script in ServerScriptService
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteFunction = ReplicatedStorage:WaitForChild("RemoteFunctionTest")
-- Script in ServerScriptService to create Part with the passed properties
local function createPart(player, partColor, partPosition)
print(player.Name .. " requested a new part")
local newPart = Instance.new("Part")
-- Use partColor and partPosition to set the Part's BrickColor and Position
newPart.BrickColor = partColor
newPart.Position = partPosition
newPart.Parent = workspace
return newPart
end
-- Bind createPart() to the `Class.RemoteFunction`'s OnServerInvoke callback
remoteFunction.OnServerInvoke = createPart
Server to Client to Server
You can use a Script to call a function on the client by calling the InvokeClient() method on a RemoteFunction, but it has the following serious risks:
For actions that don't require two-way communications, such as updating a GUI, use a RemoteEvent and communicate from the server to client.
Argument Limitations
When an event is fired or invoked, it forwards any arguments that you pass with it or to the callback function. For built-in events, follow the argument limitations on the API reference. For custom events and networking events, though the arguments can be all types of Roblox objects and Luau date types such as numbers, strings, and booleans, there are certain limitations apply to the arguments.
Non-String Indices
If any indices of a passed table are non-string types such as an Instance, userdata, or function, Roblox automatically converts those indices to strings.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local bindableEvent = ReplicatedStorage:WaitForChild("BindableEvent")
local function onEventFire(passedTable)
for k, v in pairs(passedTable) do
print(typeof(k)) --> string
end
end
bindableEvent.Event:Connect(onEventFire)
-- Fire event with a table containing a workspace instance as a key
bindableEvent:Fire({
[workspace.Baseplate] = true
})
Functions in Remotes
Functions referenced as arguments in a RemoteEvent or RemoteFunction will not be replicated across the client-server boundary, making it impossible to pass functions remotely. Instead, the resulting argument on the receiving side will be nil.
Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteEvent = ReplicatedStorage:WaitForChild("RemoteEvent")
local function testFunction()
print("Hello world!")
end
-- Fire remote event with function as an argument
remoteEvent:FireAllClients(testFunction)
LocalScript
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteEvent = ReplicatedStorage:WaitForChild("RemoteEvent")
local function onClientEvent(func)
print(func) --> nil
end
remoteEvent.OnClientEvent:Connect(onClientEvent)
Mixed Tables
If you pass a table with a mix of numeric and string keys, the values indexed by string keys are lost. In the following code sample, colorData is a mixed table. When the server receives colorData, it receives only indices [1] and [2] containing "Blue" and "Yellow". The remaining data is lost.
LocalScript
local ReplicatedStorage = game:GetService("ReplicatedStorage")local remoteEvent = ReplicatedStorage:WaitForChild("RemoteEvent")-- Mixed tablelocal colorData = {}colorData[1] = "Blue"colorData[2] = "Yellow"colorData["Color1"] = "Green"colorData["Color2"] = "Red"-- Table with two key-indexed sub-tableslocal playerData = {}playerData["CharData"] = {-- All children indexed by keyCharName = "Diva Dragonslayer",CharClass = "Knight"}playerData["Inventory"] = {-- All children numerically indexed"Sword","Bow","Rope"}remoteEvent:FireServer(colorData)remoteEvent:FireServer(playerData)
Table Identities
Tables passed as arguments to custom events are copied, meaning they will not be exactly equivalent to those provided when firing the an asynchronous event or invoking a synchronous event. Nor will tables returned to the invoker be exactly equivalent to those provided. You can demonstrate this by running the following script on a BindableFunction and observing how the table identities differ.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local bindableFunction = ReplicatedStorage:WaitForChild("BindableFunction")
bindableFunction.OnInvoke = function(passedTable)
print(tostring(passedTable))
return passedTable
end
local testTable = {message = "Hello world!"}
print(tostring(testTable))
local invokeReturn = bindableFunction:Invoke(testTable)
print(tostring(invokeReturn))
Original Identity | table: 0xf775b8bcc5e44cce |
Identity on Invocation | table: 0x03b4045c4f99ec0f |
Identity on Return | table: 0x11f690dfe56daf6e |
Empty Indices
If a table has numeric indices, and one of the values is nil, that index and its subsequent indices are lost. In the following code example, the message table has [3] = nil, so that index and [4] = "Goodbye world!" are lost in the transmission.
Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")local remoteEvent = ReplicatedStorage:WaitForChild("RemoteEvent")local message = {[1] = "Hello",[2] = "world!",[3] = nil,[4] = "Goodbye world!"}remoteEvent:FireAllClients(message)
LocalScript
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteEvent = ReplicatedStorage:WaitForChild("RemoteEvent")
local function onClientEvent(param)
print(param) --> [1] = "Hello", [2] = "world!"
end
remoteEvent.OnClientEvent:Connect(onClientEvent)
Metatables
If a table has a metatable, all of the metatable information is lost in the transfer.
In the following code sample, the NumWheels property is part of the Car metatable. When the server receives the following table, the truck table has the Name property but not the NumWheels property.
Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")local remoteEvent = ReplicatedStorage:WaitForChild("RemoteEvent")local Car = {}Car.NumWheels = 4Car.__index = Carlocal truck = {}truck.Name = "MyTruck"setmetatable(truck, Car)remoteEvent:FireAllClients(truck)
LocalScript
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteEvent = ReplicatedStorage:WaitForChild("RemoteEvent")
local function onClientEvent(param)
print(param) --> {["Name"] = "MyTruck"}
end
remoteEvent.OnClientEvent:Connect(onClientEvent)
Non-Replicated Instances
If an event or function passes a value that's only visible to the sender, Roblox doesn't replicate it across the client-server boundary and passes nil instead of the value. For example, if a Script tries to pass a descendant of ServerStorage, the client listening to the event receives a nil value because that object isn't replicable for the client.
Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
local remoteEvent = ReplicatedStorage:WaitForChild("RemoteEvent")
-- Will be received as "nil" because client can't access ServerStorage
local storedPart = Instance.new("Part")
storedPart.Parent = ServerStorage
local function onPlayerAdded(player)
remoteEvent:FireClient(player, storedPart)
end
Players.PlayerAdded:Connect(onPlayerAdded)
Similarly, if you create a part in a LocalScript and try to pass it to a Script, the server sees nil because the part isn't replicable for the server.
LocalScript
local ReplicatedStorage = game:GetService("ReplicatedStorage")local remoteEvent = ReplicatedStorage:WaitForChild("RemoteEvent")-- Will be received as "nil" because the server doesn't know about this partlocal clientPart = Instance.new("Part")clientPart.Parent = workspaceremoteEvent:FireServer(clientPart)