Lua Chat System

This is an advanced tutorial This is an advanced tutorial. This tutorial discusses object-orientated scripting with Roblox's chat system. Return to tutorial index


Allowing players to quickly and easily communicate is a key component to a multiplayer platform like Roblox. Games played on the desktop and mobile clients come with a text-based chat system. This system has many of the features one would find in other chat clients, but is also designed to be fully customizable by the developer.

Chat Structure[edit]

The Roblox chat system is built around three components: Speakers, Messages, and Channels.

A Speaker is an entity that can speak in chat. All players who join the game are automatically a speaker. Speakers can also be bots that are created and managed by code.

A Message is a container for content that a speaker enters into their chat. This can be text to send to other speakers or a command that performs a specific action in-game. Messages can also contain metadata which can be used to format the message or add extra functionality to commands.

A Channel is a section of chat where only certain Speakers can see and write messages. By default each player is automatically added to the All and System channels, although they will also be added to a Team channel if they are assigned a team. Whispers between players also use a channel per each pair of participants.

Hierarchy[edit]

The chat system is designed to take advantage of the client-server model. Channel and Speaker management is handled on the server while the client is responsible for input and the display of messages. Communication between the server and clients is handled automatically by the system through RemoteEvents.

The Chat service itself is the essential storage unit for the chat system. When a Roblox place loads (either in Client or in Studio when running or playing), all of the components of the chat system are automatically loaded into the Chat service. This includes two Folders, a Script, a LocalScript, and various ModuleScripts as children of each of the above.

ChatHierarchy.png

  • ChatServiceRunner - This Script runs the server component of the chat. In general this does not need to be modified to create custom chat behavior and functionality. When the game runs this gets cloned automatically into the ServerScriptService.
  • ChatModules - This folder is a collection of modules that are required by the ChatServiceRunner. All of the contents of this folder are required by the script and are used to create custom behavior on the server (see #Setting up Server Modules). Note: Unless you create a BoolValue inside ChatModules named "InsertDefaultModules" with value true, adding a ChatModules folder to your game will automatically disable whisper chat and colored names.
  • ChatScript - This LocalScript runs the client component of the chat. Like ChatServiceRunner, this should not need to be modified to customize chat. When the game runs this gets cloned automatically to the StarterPlayerScripts.
  • ClientChatModules - This folder contains various ModuleScripts required by the ChatScript.
    • CommandModules - Contains modules used to implement client-side chat commands.
    • MessageCreatorModules - Contains modules used to format and handle messages.
    • ChatConstants - Module used to store various constants that are shared between server and client scripts.
    • ChatSettings - Module used to store various settings to configure different aspects of the chat window.

To modify or customize the Chat system, it is recommended to make a copy of the entire service through the following steps:

  • Run Studio with the Play button
  • Copy the objects in the Chat service
  • Press the Stop button
  • Paste them into the Chat service

Chat Workflow[edit]

Before making modules to customize the chat, it is important to understand the workflow that a chat message goes through. Along with sending text messages, there are various commands built into the chat system, so every message has to be checked to see if they need to be interpreted as a command or just a text message. Even text messages can be modified and filtered in the process.

After a player has focus in the chat input and enters a character, several checks are made right away on the Client. If the character is “Escape” the input box closes and no actions are taken. If the character is anything other than “Enter” the text gets passed through “In-Progress” command processors. These are used to evaluate the text to see if any action needs to be taken. For example, when a user starts whispering with the /whisper command, as soon as a player name has been entered after the command, the input box changes to indicate that the player is now entering in a whisper channel.

When the player finishes typing and hits “Enter” the text they input is sent through several more command processors. If an In-Progress command made a custom chat state, the chat checks the state to see if a final command should be executed and if the message should continue on. If the message is allowed to continue, then the text is sent through another set of processors called “Completed” processors. If any of these processors return true, the message stops being sent. Otherwise, the message is sent to the server.

On the Client side of chat there are two types of processors: In-Progress and Completed. In-Progress will evaluate after every character has been typed, Completed will only evaluate when the player has finished typing and has hit “Enter”.

Once the message reaches the server, it goes through another set of command processors. Just like the “Completed” processors on the client, if any of these processors return true, then the message stops executing. Otherwise the message gets passed through a set of filters (including the default Roblox chat filter). Once all of this is done the message is sent to all of the Channels and appropriate Speakers.

ChatWorkflow.png

Server Modules[edit]

Modules put into ChatModules can be used for a variety of purposes. These modules can be used to manage chat channels and speakers, add filter and command functions, run chatbots, or anything else that needs to be handled on the server. To interact with the chat system, each module is passed a ChatService object.

When the ChatServiceRunner starts up, it requires each module inside of ChatModules. It expects each module to return a function as it then calls each of the modules in turn, passing in it’s ChatService object to each function. Regardless of what the module is intended to do (running a bot, adding a filter function, etc), it needs to follow this form in order to work.

-- Sample Module Framework
local function Run(ChatService)
	-- Code goes here
end
 
return Run

Adding Channels[edit]

One of the simplest things a ChatModule can do is to manage Channels. Channel objects can be created with the AddChannel function of ChatService. Note that the channel object only needs to be used when calling members of that channel (such as its properties and functions). When referring to channels from the context of ChatService or Speakers, the channel’s name is used to reference it.

local function Run(ChatService)
	local myChannel = ChatService:AddChannel("MyChannel")
end
 
return Run

Basic Channel Configuration[edit]

Channels have several properties that can be used to slightly modify them. For example, this module creates a channel and sets the Welcome Message and causes players to automatically join the channel when they enter the game.

local function Run(ChatService)
	local myChannel = ChatService:AddChannel("MyChannel")
 
-- Set the message that is shown when a user joins the channel 
	myChannel.WelcomeMessage = “Welcome to my channel!”
	-- Causes players to automatically join the channel when they enter the game
	myChannel.AutoJoin = true
end
 
return Run

Channel Events[edit]

Channels have several events that can be subscribed to. These events fire when a Message is posted to the Channel, when a Speaker leaves or joins, or when a Speaker is muted or unmuted. For example, this module will create a Channel with the name “MyChannel”. Whenever a speaker joins or leaves the channel, a system message will be sent to all of the speakers in the channel informing them of the event.

local function Run(ChatService)
	local myChannel = ChatService:AddChannel("MyChannel")
 
	local function onSpeakerJoined(speakerName)
		myChannel:SendSystemMessage(speakerName .. " has joined the channel.")
	end
 
	local function onSpeakerLeft(speakerName)
		myChannel:SendSystemMessage(speakerName .. " has left the channel.")
	end
 
	myChannel.SpeakerJoined:Connect(onSpeakerJoined)
	myChannel.SpeakerLeft:Connect(onSpeakerLeft)
end
 
return Run

Command Functions[edit]

Another powerful thing that ChatModules can do are chat commands. When a message is sent to the server, the chat will send the message through each command function that has been registered to the ChatService and relevant Channel. These functions are sent the speaker, message, and channel that the message is being sent to. The function can take any action that it needs to and then return true or false. If the function returns true, then the message stops being processed by the chat system. It will not be sent to any more command functions nor will it be displayed in the chat window. If the function returns false, then the message continues through all of the other command functions. If none of the command functions returns true, the message will then be sent through filters and then will be displayed.

Command Functions are often used to implement what are called “Admin Commands” - text commands that certain users can use to manipulate the game state through specific text said in the chat.

In this example a ChatModule is used to create a Part if a player types “/part” in the chat. Note that this function returns true if a Part was created which will stop the message from proceeding and no message will be displayed. If a part is not created, this function needs to return false so that the message can continue working through the system.

local function Run(ChatService)
	local function createPart(speakerName, message, channelName)
		if string.sub(message, 1, 5) == "/part" then
			local newPart = Instance.new("Part")
			newPart.Parent = game.Workspace
			return true
		end
		return false
	end
 
	ChatService:RegisterProcessCommandsFunction("createPart", createPart)
end
 
return Run
Both Channels and ChatService itself can have chat commands. ChatService command processors will run on every message that is sent to the server, while Channel commands will only run if the message was sent to the channel the command is registered to.

Filter Functions[edit]

Messages that are not stopped by a Command Function will go through all of the filter functions that are registered to the ChatService and relevant Channels. Each filter function is passed the speaker, message object, and channel name. Any changes made to the message object will persist and each following filter function will see the updated message. Note that unlike a command function, filter functions do not need to return a value.

In this example, a simple filter function is registered to make every message appear in lowercase.

local function Run(ChatService)
	local function makeLowercase(sender, messageObject, channelName)
		messageObject.Message = string.lower(messageObject.Message)
	end
 
	ChatService:RegisterFilterMessageFunction("makeLowercase", makeLowercase)
end
 
return Run

Client Modules[edit]

Modules put into ClientChatModules can be used to make custom behavior for clients. These modules are divided into two different folders: CommandModules and MessageCreatorModules.

Command Modules[edit]

CommandModules work very similarly to modules on the server that register Command functions. These modules define functions that will fire after the player has entered in text. That text can be read and the command can either let the message through to the server or stop the progress of the message. The main difference is that CommandModules can be evaluated either when the user has hit “Enter”, or after every character as they are typed. Commands that are evaluated at the end of the message are tagged with COMPLETED_MESSAGE_PROCESSOR, commands that are evaluated after each character are tagged with IN_PROGRESS_MESSAGE_PROCESSOR.

In both types of commands, the module must return a dictionary that says what type of processor the command should use, and what function to execute when the processor is called. For example, a Completed Message Processor should take the form:

local util = require(script.Parent:WaitForChild("Util"))
 
function ProcessMessage(message, ChatWindow, ChatSettings)
 
end
 
return {
	[util.KEY_COMMAND_PROCESSOR_TYPE] = util.COMPLETED_MESSAGE_PROCESSOR,
	[util.KEY_PROCESSOR_FUNCTION] = ProcessMessage
}

Note that the KEY_COMMAND_PROCESSOR_TYPE enum is defined in the Util ModuleScript inside of the CommandModules folder. It is recommended to always require this module in Command Modules.

Completed Message Commands[edit]

Completed Message Commands are evaluated when the user has finished typing and has hit “Enter”. The function of the processor is passed the Message object, the client’s ChatWindow, and the ChatSettings table. If the function returns true, then the message stops being processed and will not be sent to the server. Otherwise it will be sent through all of the other processors and eventually to the server if none of the other processors stop it.

For example, the following processor will remove the oldest message in the current channel if the user enters the command “/last”.

local util = require(script.Parent:WaitForChild("Util"))
 
function ProcessMessage(message, ChatWindow, ChatSettings)
	if string.sub(message, 1, 5) == "/last" then
		local currentChannel = ChatWindow:GetCurrentChannel()
		if (currentChannel) then
			currentChannel:RemoveLastMessageFromChannel()
		end
		return true
	end
	return false
end
 
return {
	[util.KEY_COMMAND_PROCESSOR_TYPE] = util.COMPLETED_MESSAGE_PROCESSOR,
	[util.KEY_PROCESSOR_FUNCTION] = ProcessMessage
}

In Progress Commands[edit]

In progress commands are evaluated every time the player types a character into the chat input. For example, the following code plays a clack after every keypress to make it sound like the player is typing on a typewriter:

local util = require(script.Parent:WaitForChild("Util"))
local keyEffect = Instance.new("Sound")
keyEffect.SoundId = "rbxassetid://12221976"
keyEffect.Parent = script
 
function ProcessMessage(message, ChatWindow, ChatBar, ChatSettings)
	keyEffect:Play()
end
 
return {
	[util.KEY_COMMAND_PROCESSOR_TYPE] = util.IN_PROGRESS_MESSAGE_PROCESSOR,
	[util.KEY_PROCESSOR_FUNCTION] = ProcessMessage
}

Custom State[edit]

In Progress Commands are often used to make a custom state for the chat to send messages to specific players instead of just the current channel. For example, the Whisper and Team chat systems use In Progress Commands to see if the player has typed “/whisper” or “/team” respectively, and will send the finished message to only the appropriate players.

A custom chat state overrides all other commands, either in-progress or completed. It will remain this way until ChatBar.ResetCustomState is called, which will remove the custom state and revert back to normal chat behavior.

A custom state is expected to be table with the following functions:

  • TextUpdated - called when the text in the input box changes
  • GetMessage - called after the player has finished entering the message and hits “Enter”. This function is expected to return a string.
  • ProcessCompletedMessage - called as the message is being processed. A custom state processor will always fire before the Completed Message processors. Like other processors this function should return true if the message should stop being sent, otherwise it should return false.
  • Destroy - called after the message has been sent. Should be used to clean up anything setup by the custom state

In order to use a Custom State, the ProcessMessage function of the Command Module must return the state. A basic custom state would take the following form:

local util = require(script.Parent:WaitForChild("Util"))
 
local customState = {}
customState.__index = customState
 
function customState:TextUpdated()
	print("text updated")
end
 
function customState:GetMessage()
	print("get message")
	return self.TextBox.Text
end
 
function customState:ProcessCompletedMessage()
	print("process message")
	return false
end
 
function customState:Destroy()
	print("destroy custom state")
	self.Destroyed = true
end
 
function customState.new(ChatWindow, ChatBar, ChatSettings)
	local obj = {}
	setmetatable(obj, customState)
 
	obj.Destroyed = false
	obj.ChatWindow = ChatWindow
	obj.ChatBar = ChatBar
	obj.ChatSettings = ChatSettings
	obj.TextBox = ChatBar:GetTextBox()
	obj.MessageModeLabel = ChatBar:GetMessageModeTextLabel()	
 
	return obj
end
 
local function ProcessMessage(message, ChatWindow, ChatBar, ChatSettings)
	return customState.new(ChatWindow, ChatBar, ChatSettings)
end
 
return {
	[util.KEY_COMMAND_PROCESSOR_TYPE] = util.IN_PROGRESS_MESSAGE_PROCESSOR,
	[util.KEY_PROCESSOR_FUNCTION] = ProcessMessage
}

One of the chief advantages of using a custom state is that a module can edit the chat bar and its containing text while the player is typing both in terms of function and looks, and then easily reset it afterwards (once a message is sent a custom state is automatically removed and everything is reset back to normal). For example, this code sets up a custom state that only allows 20 characters to be shown in the textbox at at time. If the player keeps typing, characters at the beginning of the string are temporarily removed. When the player sends the message, all of the removed characters are added back to the message.

local util = require(script.Parent:WaitForChild("Util"))
 
local oneLineState = {}
oneLineState.__index = oneLineState
 
function oneLineState:TextUpdated()
	local text = self.TextBox.Text
	local length = string.len(text)
	if length > 20 then
		local chopLength = length - 20
		local addToPrefix = string.sub(text, 1, chopLength)
		self.Prefix = self.Prefix .. addToPrefix
		self.TextBox.Text = string.sub(text, chopLength + 1)
	end
end
 
function oneLineState:GetMessage()
	local fullString = self.Prefix .. self.TextBox.Text
	return fullString
end
 
function oneLineState:ProcessCompletedMessage()
	return false
end
 
function oneLineState:Destroy()
	self.Destroyed = true
end
 
function oneLineState.new(ChatWindow, ChatBar, ChatSettings)
	local obj = {}
	setmetatable(obj, oneLineState)
 
	obj.Destroyed = false
	obj.ChatWindow = ChatWindow
	obj.ChatBar = ChatBar
	obj.ChatSettings = ChatSettings
	obj.TextBox = ChatBar:GetTextBox()
	obj.MessageModeLabel = ChatBar:GetMessageModeTextLabel()	
 
	obj.Prefix = ""	
 
	return obj
end
 
local function ProcessMessage(message, ChatWindow, ChatBar, ChatSettings)
	return oneLineState.new(ChatWindow, ChatBar, ChatSettings)
end
 
return {
	[util.KEY_COMMAND_PROCESSOR_TYPE] = util.IN_PROGRESS_MESSAGE_PROCESSOR,
	[util.KEY_PROCESSOR_FUNCTION] = ProcessMessage
}

As mentioned before, once a message has been sent any custom state is removed and the chat is restored to normal. If it is needed to reset a custom state before sending the message, the state can be reset with ChatBar:ResetCustomState(). Note that this will remove focus from the chat bar’s text box as well.

Message Creator Modules[edit]

The other type of module that can be used on in the client component of the chat is a Message Creator module. This type of module is used to create the GUI elements in the chat window to display the message. Each type of Message Creator defines a new message type so different messages can be created with different formatting. Moreover, more GUI elements can be added to the display of messages this way which allows for images, buttons, etc.

Message Modules require setup in several different locations. For each message type, there must be a ModuleScript inside of MessageCreatorModules. Also, the ModuleScript ChatConstants needs to be edited to include the new message type. Last, Message Creators are only used if a server component of chat creates a new message with the given message type. This means that typically a ChatModule is also created (or an existing one edited) to use a new Message Creator.

To illustrate the structure and setup of a Message Creator, the following example will go through making a bot that says the time every 5 seconds, and the message that is sent gets a red background.

To start, the ChatConstants ModuleScript needs to add a field for the new type of message.

-- ChatConstants
local module = {}
 
---[[ Message Types ]]
module.MessageTypeDefault = "Message"
module.MessageTypeSystem = "System"
module.MessageTypeMeCommand = "MeCommand"
module.MessageTypeWelcome = "Welcome"
module.MessageTypeSetCore = "SetCore"
module.MessageTypeWhisper = "Whisper"
module.MessageTypeTime = "Time" 
module.MajorVersion = 0
module.MinorVersion = 2
 
return module

The bot itself is created in a new ChatModule on the server. Note that a filter function is used to add the new message type to the messages that the bot sends.

-- New ModuleScript to be placed in ChatModules
local Chat = game:GetService("Chat")
local ReplicatedModules = Chat:WaitForChild("ClientChatModules")
local ChatConstants = require(ReplicatedModules:WaitForChild("ChatConstants"))
 
local function Run(ChatService)
	local timeBot = ChatService:AddSpeaker("TimeBot")
	timeBot:JoinChannel("All")
 
	local function addMessageType(speaker, messageObject, channelName)
		if speaker == "TimeBot" then
			messageObject.MessageType = ChatConstants.MessageTypeTime
		end
	end
 
	ChatService:RegisterFilterMessageFunction("TimeBotFilter", addMessageType)	
 
	spawn(function()
		while wait(5) do
			timeBot:SayMessage("The current time is: " .. os.time(), "All", {})
		end
	end)
end
 
return Run

Last, a Message Creator module must be made. This module must return a dictionary with two elements: the type of the message, indexed with KEY_MESSAGE_TYPE, and the function to call when creating the message GUI elements, indexed with KEY_CREATOR_FUNCTION.

The function stored by KEY_CREATOR_FUNCTION needs to return a dictionary with several components. First, it needs to include a Frame and TextLabel which will be displayed in the chat window. These can be created with the function util:CreateBaseMessage. The dictionary also needs to include a function to run if the text of the message updates. When messages first appear in the client, they have blank placeholder text while the message is being processed and filtered, so message objects like this need to handle what happens when they get a call to update. Next, the dictionary needs to include a function to determine the height of the frame. This function often calls the util:GetMessageHeight function. Last, the dictionary needs to include several functions that define how the elements should fade when the window fades. There is another utility function for this: util:CreateFadeFunctions.

-- new ModuleScript to be included in MessageCreatorModules
local messageCreatorModules = script.Parent
local util = require(messageCreatorModules:WaitForChild("Util"))
local clientChatModules = messageCreatorModules.Parent
 
local ChatSettings = require(clientChatModules:WaitForChild("ChatSettings"))
local ChatConstants = require(clientChatModules:WaitForChild("ChatConstants"))
 
 
local function CreateMessageLabel(messageData, channelName)
	-- Create the GUI objects for the Frame and TextLabel to hold the message
	local BaseFrame, BaseMessage = util:CreateBaseMessage("", ChatSettings.DefaultFont, ChatSettings.ChatWindowTextSize, ChatSettings.DefaultMessageColor)
 
	-- Change the background of the Frame to red
	BaseFrame.BackgroundColor3 = Color3.new(1,0,0)
	BaseFrame.BackgroundTransparency = 0
 
	-- Handle updating placeholder message text
	local function UpdateTextFunction(messageObject)
        if messageObject.IsFiltered then
		   BaseMessage.Text = messageObject.Message
        end
	end
    UpdateTextFunction(messageData)
 
	-- Use util function to determine height of frame
	local function GetHeightFunction(xSize)
		return util:GetMessageHeight(BaseMessage, BaseFrame, xSize)
	end
 
	-- Create fade functions that are called when the chat window fades
	local FadeParameters = {}
	FadeParameters[BaseMessage] = {
		TextTransparency = {FadedIn = 0, FadedOut = 1},
		TextStrokeTransparency = {FadedIn = 0.75, FadedOut = 1}
	}
	local FadeInFunction, FadeOutFunction, UpdateAnimFunction = util:CreateFadeFunctions(FadeParameters)
 
	-- Return dictionary that defines the message label
	return {
		[util.KEY_BASE_FRAME] = BaseFrame,
		[util.KEY_BASE_MESSAGE] = BaseMessage,
		[util.KEY_UPDATE_TEXT_FUNC] = UpdateTextFunction,
		[util.KEY_GET_HEIGHT] = GetHeightFunction,
		[util.KEY_FADE_IN] = FadeInFunction,
		[util.KEY_FADE_OUT] = FadeOutFunction,
		[util.KEY_UPDATE_ANIMATION] = UpdateAnimFunction
	}
end
 
return {
	[util.KEY_MESSAGE_TYPE] = ChatConstants.MessageTypeTime,
	[util.KEY_CREATOR_FUNCTION] = CreateMessageLabel
}