[Edit 27/12/24]: Seems like this topic has been getting some views recently.
This tutorial was written quite poorly, so I’ll soon rewrite it.
Heya!
In this tutorial, I’ll show you how to create a simple Global Chat system using TextChatService.
Please read this before proceeding:
This tutorial uses TextChatService (the new one)! If you’re using LegacyChatService (the old one), you can try this guide, or use this tutorial, by Hashtag Blu!
This tutorial is focused on creating the mere global chat system (that allows users from multiple servers to talk to each other). For other features such as custom commands, prefixes and so, you will have implement them yourself. I don’t know what you want. Except colors. I’ll let you know how to do that.
If you don’t want to bother following the tutorial, go below for the code. Except it’s more fun to build it yourself! You can add anything you’d like.
Tutorial
Setup
-
Prepare a RemoteEvent and place it under ReplicatedStorage. Name it anything you’d like. For this tutorial, we’ll name it “Messenger”.
-
Create two scripts:
-
A Script under ServerScriptService
-
A LocalScript under StarterPlayer - StarterPlayerScripts
-
The next part? The scripting!
Client code
- Let’s name some variables below.
---- Services ----
local TextChatService = game:GetService('TextChatService')
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local Players = game:GetService('Players')
---- Variables ----
local Messenger = ReplicatedStorage:WaitForChild('Messenger') -- Our RemoteEvent
local channel = TextChatService:WaitForChild('TextChannels').RBXGeneral
RBXGeneral
is the default general TextChannel where users send their messages. We’ll call methods using the general channel.
- First, we want to listen for the local player sending a message. For this, we’ll use
TextChatService.SendingMessage
. -
SendingMessage
will fire whenTextChannel:SendAsync()
is called from the client. When you send a message,SendAsync()
will be called by the default chat scripts, which will trigger the event.
TextChatService.SendingMessage:Connect(function(message)
Messenger:FireServer(message.Text)
end)
SendingMessage
passes a TextChannelMessage
as parameter, which contains information about the message. For now, we only want the message, so we get it by doing message.Text
, which is the string of the actual message the player sent.
- Next, we’ll want to listen for when the server receives a message from the other players. You can send the data however you want, but I’ll use two variables here: the player who sent it, and their message.
Messenger.OnClientEvent:Connect(function(fromPlayer, playerMessage)
channel:DisplaySystemMessage(fromPlayer .. ": " .. playerMessage)
end)
If you prefer to use string interpolation instead:
channel:DisplaySystemMessage(`{fromPlayer}: {playerMessage}`)
Because TextChatService doesn’t have a method that allows us to send messages as a player, we’ll use a System message instead.
Your code should look like this:
---- Services ----
local TextChatService = game:GetService('TextChatService')
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local Players = game:GetService('Players')
---- Variables ----
local Messenger = ReplicatedStorage:WaitForChild('Messenger') -- Our RemoteEvent
local channel = TextChatService:WaitForChild('TextChannels').RBXGeneral
TextChatService.SendingMessage:Connect(function(data)
Messenger:FireServer(data.Text)
end)
Messenger.OnClientEvent:Connect(function(fromPlayer, playerMessage)
channel:DisplaySystemMessage(data.fromPlayer .. ": " .. data.Message)
end)
This is the client part! Now for the complicated part…
Server code
- Again, some variable declarations…
---- Services ----
local TextChatService = game:GetService("TextChatService")
local MessagingService = game:GetService("MessagingService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
---- Variables ----
local Messenger = ReplicatedStorage:WaitForChild("Messenger")
local topic = "GlobalChat"
-
A
topic
is what MessagingService uses to listen to and publish messages. Like DataStores, it’s a unique string that identifies it. When listening or publishing a message, the topic must be the same one, or messages will not deliver. And again, like DataStores, you can name them however you’d like, as long as you… remember it. -
First, let’s begin with the function that will send the message to other servers. For this, we’ll use
MessagingService:PublishAsync()
, which, when called, well, sends the message to other servers.
Messenger.OnServerEvent:Connect(function(player, message)
end)
Now here’s the thing:
- You cannot send a dictionary via MessagingService! You can only send a piece of string, number or boolean, and they have to be UTF-8 characters. This means that we cannot send something like this:
local data = {
Player = player,
Message = "hi"
}
- To compensate that, we’ll include both the player and their message as a single string, by separating them with a few characters:
local dataToBeSent = player.Name .. '\\' .. message
The slash should be enough. Course, you can also use other ways of separating it. But your question must be “how would i separate that when i get it from other servers?” We’ll figure that out in a second.
- Now, we’ll put that inside the event connection, then send it on the way using
PublishAsync()
!
Messenger.OnServerEvent:Connect(function(player, message)
local dataToBeSent = player.Name .. '\\' .. message
MessagingService:PublishAsync(topic, dataToBeSent)
end)
For calls to any API service, it is recommended to wrap them in pcalls
since they tend to error (which would error your code as well).
If you do want to wrap it in a protected call:
Messenger.OnServerEvent:Connect(function(player, message)
local dataToBeSent = player.Name .. '\\' .. message
local success = pcall(MessagingService.PublishAsync, MessagingService, topic, dataToBeSent)
end)
PublishAsync()
takes two parameters:
- Logically, the topic the message it’ll be sent to,
- and the actual data to be sent.
That should be it! Now, to listen for the message being sent from other servers:
- We want to initialise the chat by subscribing to the topic. Using
MessagingService.SubscribeAsync
, we begin listening to the topic.
MessagingService:SubscribeAsync(topic, function(message)
- The first argument of SubscribeAsync is
topic
, which explained before, can be any name you desire. Remember that it must be the same topic you send the messages to, and receive messages from! - The second argument is a function that will be called when
PublishAsync()
is called from another server. It passes an argument for the callback function, containing what the server sent as a dictionary.
Remember how we sent the player’s message? As a string, yes! Now, what you’ll receive from PublishAsync()
will be a dictionary instead, that contains:
-
Data
, what you’ve provided to be sent, -
Sent
, UNIX time in seconds, showing when the message was created and sent.
In this function we’ll only useData
, the message we provided.
Remember that we had to concatenate the player and the message for it to send? Now let’s separate that!
local fromPlayer, playerMessage = string.match(message.Data, "(.-)\\(.*)")
“What the heck does this mean?”
- Using
string.match
, we find the matching string. Previously, we concatenated it asplayer\\message
, so now we’re separating it by finding the double backslash. - The patterns
-
(.-)
will look for everything before the double backslash (the player), -
(.*)
will look for everything else after the double backslash (the message).
-
That means that the pattern (.-)\\(.*)
will:
- Return the first string, which is everything before the double backslash,
- and return a second string, everything after said backslash.
This ensures that we’re not separating anything in the wrong way, even if the message happens to contain the actual backslashes. And, I mean, the player’s name can’t contain backslashes, so it’s okay.
Now let’s send that to every player in the server! Oh, wait, hold on. What happens if the player who’s sending is in the server? We would have two messages for that player! Let’s fix that with “a single line”.
if table.find(Players:GetPlayers(), Players:GetPlayerByUserId(Players:GetUserIdFromNameAsync(fromPlayer))) then return end
- We cannot compare
Player == string
, so we have to convert the username to a Player instance in order to compare. For that, we first get their UserId, then their Player instance, as there’s no such thing asGetPlayerFromUsernameAsync
to directly turn the username into a Player instance. - If found (which means they’re in the server), don’t send this to all players, because everyone else would already have their message.
Alright, that should be it, right?
Wooah, hold on!
The message is not filtered! Let’s appropriately filter the player’s message.
- Because <13 and 13+ have different filtering restrictions, we want to iterate through all players first. Can we ignore this? Yes, you could filter for broadcast, but let’s do it the correct way, which is, in context of Chat.
local objectSuccess, object = pcall(function()
return TextService:FilterStringAsync(message, fromUserId, Enum.TextFilterContext.PublicChat)
end)
if not objectSuccess then return end
-- We want to get the FilterObject before filtering its string
local filterSuccess, result = pcall(function()
return object:GetChatForUserAsync(forUserId)
-- ensures we're filtering this for the player appropriately
end)
How would I actually implement this into my code?
- Remember, let’s iterate through all players first. After the check to see if the player is already in the server:
if not table.find(Players:GetPlayers(), Players:GetPlayerByUserId(Players:GetUserIdFromNameAsync(fromPlayer))) then
for _, player in Players:GetPlayers() do
local objectSuccess, object = pcall(function()
return TextService:FilterStringAsync(
playerMessage, -- the message
Players:GetUserIdFromNameAsync(fromPlayer), -- from who
Enum.TextFilterContext.PublicChat -- Context for filtering
)
end)
if not objectSuccess then return end -- If error, do what you want. Here, do nothing.
local filterSuccess, result = pcall(function()
return object:GetChatForUserAsync(player.UserId) -- Filter for each player according to their restrictions
end)
if not filterSuccess then return end -- again, manage the error however you'd like.
Messenger:FireClient(player, fromPlayer, result) -- we send the payload - Player, and their now filtered message!
end
end
What to do in case of an error?
- Your choice, really. Don’t send anything, or send a chat warning as a system message. NEVER send an unfiltered string, even if it actually is fine.
That would be the server part! Your code should look like this.
---- Services ----
local TextChatService = game:GetService("TextChatService")
local MessagingService = game:GetService("MessagingService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
---- Variables ----
local Messenger = ReplicatedStorage:WaitForChild("Messenger")
local topic = "GlobalChat"
Messenger.OnServerEvent:Connect(function(player, message)
local dataToBeSent = player.Name .. '\\' .. message
MessagingService:PublishAsync(topic, dataToBeSent)
end)
MessagingService:SubscribeAsync(topic, function(message)
local fromPlayer, playerMessage = string.match(message.Data, "(.-)\\(.*)")
if not table.find(Players:GetPlayers(), Players:GetPlayerByUserId(Players:GetUserIdFromNameAsync(fromPlayer))) then
for _, player in Players:GetPlayers() do
local objectSuccess, object = pcall(function()
return TextService:FilterStringAsync(
playerMessage, -- the message
Players:GetUserIdFromNameAsync(fromPlayer),
Enum.TextFilterContext.PublicChat -- Context for filtering
)
end)
if not objectSuccess then return end
local filterSuccess, result = pcall(function()
return object:GetChatForUserAsync(player.UserId)
end)
if not filterSuccess then return end
Messenger:FireClient(player, fromPlayer, result)
end
end
end)
You’re done!
Let’s put it to the test!
That’s my friend in another server, yup yup, that’s it!
Possible FAQ
How do I use Usernames for everyone instead of Display Names?
- Refer to this post, it may help you out!
How would I color the usernames from other servers?
- TextChatService supports Rich Text, which means we can color the text, in this case, usernames. Given the DisplayChannelMessage method, we do this instead:
local r, g, b = 123, 123, 123
currentChannel:DisplaySystemMessage(`<font color='rgb({r}, {g}, {b})'>{fromPlayer}:</font> {playerMessage}`)
Replace the RGB value with your desired colors. To get the colors different usernames like how Roblox does, use this code instead:
local NameColors = {
BrickColor.new("Bright red").Color,
BrickColor.new("Bright blue").Color,
BrickColor.new("Earth green").Color,
BrickColor.new("Bright violet").Color,
BrickColor.new("Bright orange").Color,
BrickColor.new("Bright yellow").Color,
BrickColor.new("Light reddish violet").Color,
BrickColor.new("Brick yellow").Color,
}
function GetPlayerChatColor(playerName: string)
local value = 0
for index = 1, #playerName do
local calculatedValue = string.byte(
string.sub(playerName, index, index)
)
local reverseIndex = #playerName - index + 1
if #playerName % 2 == 1 then
reverseIndex -= 1
end
if reverseIndex % 4 >= 2 then
calculatedValue = -calculatedValue
end
value += calculatedValue
end
return NameColors[(value % #NameColors) + 1].R, NameColors[(value % #NameColors) + 1].G, NameColors[(value % #NameColors) + 1].B
end
local r, g, b = getPlayerChatColor(fromPlayer)
currentChannel:DisplaySystemMessage(`<font color='rgb({r}, {g}, {b})'>{fromPlayer}:</font> {playerMessage}`)
How would I add chat prefixes, such as tags, for players from other servers?
- This guide will show you how to!
Limitations
Limitations are subjected to MessagingService limits. Refer to the documentation for it.
This chat system has no limits on how many players can be on it! To implement such limits, you’ll require some advanced scripting skills using MemoryStoreService.
Thank you!
If this tutorial helped you, do leave a like!
If something does not work, it means I may have written something wrong. Let me know and I’ll fix it!