Quick Chat for Gamepad (A Baseline)

With Roblox now having Chat on Console with TextChatService, I thought of making a quick chat system for Gamepad input. Specifically, I made a system like Rocket League with the DPad.

Video:


File: Gamepad Quick Chat.rbxl (71.4 KB)


Local Script:

click to open
-- Services & RemoteEvents:
local TextChatService = game:GetService("TextChatService")
local generalChannel: TextChannel = TextChatService:WaitForChild("TextChannels").RBXGeneral
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local TweenService = game:GetService("TweenService")
local RemoteEventsFolder_ForQuickChatSystem = game.Workspace:WaitForChild("QuickChat_RE_Folder")
local SendQuickChatToAllOtherPlayers = RemoteEventsFolder_ForQuickChatSystem.SendQuickChatToAllOtherPlayers
local UpdateUI = RemoteEventsFolder_ForQuickChatSystem.UpdateUI
local TellPlayerNoSpamming = RemoteEventsFolder_ForQuickChatSystem.TellPlayerNoSpamming

-- Local Settings:
local useQuickChatUIAnimations = true
local shouldThisScriptForceOpenChatIfClosed = true
local idleTimeBeforeClosingUI = 3 -- in seconds

-- Logic variables:
local menuIsOpen = false
local playerInteractedTwice = false
local whatMenuIsOpen = nil -- nil, "DPadUp", etc.
local mainOptionsFrame = script.Parent.CanvasGroup.Frame.Options
local canPlayEvenChat = true
if not TextChatService:CanUserChatAsync(game.Players.LocalPlayer.UserId) then
	--warn("this player should not be allowed to chat!")
	canPlayEvenChat = false
end

local function OpenUI()
	script.Parent.Enabled = true
	if useQuickChatUIAnimations then
		script.Parent.CanvasGroup.GroupTransparency = 1
		local tweenInfoFadeIn = TweenInfo.new(0.25,Enum.EasingStyle.Exponential,Enum.EasingDirection.In)	
		local TweenFadeIn = TweenService:Create(script.Parent.CanvasGroup,tweenInfoFadeIn,{ GroupTransparency = 0 })
		TweenFadeIn:Play()
		TweenFadeIn.Completed:Wait()		
	end
end
local function closeUI()
	if useQuickChatUIAnimations then
		if useQuickChatUIAnimations then
			local tweenInfoFadeOut = TweenInfo.new(0.25,Enum.EasingStyle.Exponential,Enum.EasingDirection.Out)	
			local TweenFadeOut = TweenService:Create(script.Parent.CanvasGroup,tweenInfoFadeOut,{ GroupTransparency = 1 })
			TweenFadeOut:Play()
			TweenFadeOut.Completed:Wait()		
		end
	else
		script.Parent.Enabled = false
	end
end

UserInputService.InputBegan:Connect(function(input)	
	if input.KeyCode == Enum.KeyCode.DPadUp or input.KeyCode == Enum.KeyCode.DPadDown or input.KeyCode == Enum.KeyCode.DPadLeft or input.KeyCode == Enum.KeyCode.DPadRight then
		local nameOfCurrentInput = input.KeyCode.Name
		if not menuIsOpen then
			menuIsOpen = true
			whatMenuIsOpen = nameOfCurrentInput
			script.Parent.CanvasGroup.Frame.underText.Text = nameOfCurrentInput.."Menu"
			-- Get string info from Server
			UpdateUI:FireServer(whatMenuIsOpen)
			
			playerInteractedTwice = false
			startTimer() -- so menu automatically goes away if player doesn't continue input
		else
			-- menu is open
			if not (whatMenuIsOpen == nil) then
				playerInteractedTwice = true
				SendQuickChatToAllOtherPlayers:FireServer(whatMenuIsOpen, nameOfCurrentInput)
				
				-- force open chat so player sees message:
				if shouldThisScriptForceOpenChatIfClosed then
					if canPlayEvenChat  then
						if game:GetService("StarterGui"):GetCore("ChatActive") ~= true then -- if chat is not open
							game:GetService("StarterGui"):SetCore("ChatActive", true) -- open it
						end
					else
						print("this player cannot chat")
					end
				end
				
				-- Clean up:
				nameOfCurrentInput = nil -- do I even need to set local thing here to nil or is that auto Roblox?
				cleanUp()
			else
				warn("error when trying to quick chat! - menu open is nil")
			end
		end
	end
end)

UpdateUI.OnClientEvent:Connect(function(listOfQuickChatStringsForUI)	
	mainOptionsFrame.Up.TextLabel.Text = listOfQuickChatStringsForUI.DPadUp
	mainOptionsFrame.Left.TextLabel.Text = listOfQuickChatStringsForUI.DPadLeft
	mainOptionsFrame.Right.TextLabel.Text = listOfQuickChatStringsForUI.DPadRight
	mainOptionsFrame.Down.TextLabel.Text = listOfQuickChatStringsForUI.DPadDown
	
	OpenUI()
end)

function cleanUp()
	whatMenuIsOpen = nil
	menuIsOpen = false
	closeUI()
end

function startTimer()
	local startTime = os.time()
	local thread_kinda
	thread_kinda = RunService.Heartbeat:Connect(function(deltaTime)
		if not playerInteractedTwice then
			local currentTime = os.time()
			local timePast = currentTime - startTime
			if timePast >= idleTimeBeforeClosingUI then
				cleanUp()
				thread_kinda:Disconnect()
			end
		else
			startTime = nil
			thread_kinda:Disconnect()
		end
	end)
	playerInteractedTwice = false
end

SendQuickChatToAllOtherPlayers.OnClientEvent:Connect(function(fullMessage_withPrefix)
	generalChannel:DisplaySystemMessage(fullMessage_withPrefix)
end)

TellPlayerNoSpamming.OnClientEvent:Connect(function(messageToDisplay,metaData)	
	generalChannel:DisplaySystemMessage(messageToDisplay,metaData)
end)

-- From @be_nj here: https://devforum.roblox.com/t/properties-of-displaysystemmessage-textchatservices/2051223/6
TextChatService.OnIncomingMessage = function(textChatMessage)	
	if textChatMessage.Metadata == "QuickChatSystem_TellPlayerNoSpamming" then
		local overrideProperties = Instance.new("TextChatMessageProperties")
		overrideProperties.Text = string.format("<font color='#FF0000'>%s</font>", textChatMessage.Text)
		return overrideProperties
	else
		return nil
	end
end

Server Script:

click to open
local RemoteEventsFolder_ForQuickChatSystem = game.Workspace:WaitForChild("QuickChat_RE_Folder")
local SendQuickChatToAllOtherPlayers = RemoteEventsFolder_ForQuickChatSystem.SendQuickChatToAllOtherPlayers
local UpdateUI = RemoteEventsFolder_ForQuickChatSystem.UpdateUI
local TellPlayerNoSpamming = RemoteEventsFolder_ForQuickChatSystem.TellPlayerNoSpamming

local listOfSetQuickChatStrings = {
	DPadUp = {
		DPadUp = "up 1 (up)", -- up
		DPadLeft = "up 2 (left)", -- left
		DPadRight = "up 3 (right)", -- right
		DPadDown = "up 4 (down)" -- down
	},
	DPadDown = {
		DPadUp = "down 1 (up)", -- up
		DPadLeft = "down 2 (left)", -- left
		DPadRight = "down 3 (right)", -- right
		DPadDown = "down 4 (down)" -- down
	},
	DPadLeft = {
		DPadUp = "left 1 (up)", -- up
		DPadLeft = "left 2 (left)", -- left
		DPadRight = "left 3 (right)", -- right
		DPadDown = "left 4 (down)" -- down
	},
	DPadRight = {
		DPadUp = "right 1 (up)", -- up
		DPadLeft = "right 2 (left)", -- left
		DPadRight = "right 3 (right)", -- right
		DPadDown = "right 4 (down)" -- down
	},
}

-- Spam Settings: ---
local timeForCheckingSpam = 0.5
local timeoutTimeForSpamming = 3 
local messageToPlayerIfTheyTryToSpam = "No Spamming! Quick Chat disabled for "..timeoutTimeForSpamming.." seconds."
----

SendQuickChatToAllOtherPlayers.OnServerEvent:Connect(function(player,whatMenuIsOpen, nameOfCurrentInput)
	local function isValidInput()
		local valToReturn = false
		if listOfSetQuickChatStrings[whatMenuIsOpen] and listOfSetQuickChatStrings[whatMenuIsOpen][nameOfCurrentInput] then
			valToReturn = true
		end	
		--print("isValidInput() ReturnVal: "..tostring(valToReturn))
		return valToReturn
	end
	
	local function isATimeoutInPlace()
		local valToReturn = false
		
		local quickChatSystem_IsPlayerInTimeout_BoolVal
		local success_Timeout, response_Timeout = pcall(function()
			quickChatSystem_IsPlayerInTimeout_BoolVal = player["quickChatSystem_IsPlayerInTimeout_BoolVal"]
		end)

		if success_Timeout then
			valToReturn = quickChatSystem_IsPlayerInTimeout_BoolVal.Value
		else
			-- add it
			local BoolVal = Instance.new("BoolValue")
			BoolVal.Name = "quickChatSystem_IsPlayerInTimeout_BoolVal"
			BoolVal.Value = false
			BoolVal.Parent = player
		end
		
		--print("isATimeoutInPlace() ReturnVal: "..tostring(valToReturn))
		return valToReturn
	end
	
	local function isThisSpam()
		local valToReturn = false
		local quickChatSystem_TimeOfLastQuickChat_NumVal
		local success, response = pcall(function()
			quickChatSystem_TimeOfLastQuickChat_NumVal = player["quickChatSystem_TimeOfLastQuickChat_NumVal"]
		end)
		
		--print("isThisSpam() -- pcall:")
		--print(success,response)
		
		if success then
			--print("quickChatSystem_TimeOfLastQuickChat_NumVal.Value // "..tostring(quickChatSystem_TimeOfLastQuickChat_NumVal.Value))
			local timeDiff = os.time() - quickChatSystem_TimeOfLastQuickChat_NumVal.Value
			--print("timeDiff // "..tostring(timeDiff))
			--print("timeForCheckingSpam // "..tostring(timeForCheckingSpam))
			
			if timeDiff <= timeForCheckingSpam then
				valToReturn = true
				
				if not player["quickChatSystem_IsPlayerInTimeout_BoolVal"].Value then
					player["quickChatSystem_IsPlayerInTimeout_BoolVal"].Value = true
				end
			else
				-- it's not spam, make it the new lack quick chat time!
				player["quickChatSystem_TimeOfLastQuickChat_NumVal"].Value = os.time()
			end
		else
			-- add it
			local NumVal = Instance.new("NumberValue")
			NumVal.Name = "quickChatSystem_TimeOfLastQuickChat_NumVal"
			NumVal.Value = os.time()
			NumVal.Parent = player
		end
		
		--print("isThisSpam() ReturnVal: "..tostring(valToReturn))
		return valToReturn
	end
	
	local function sendMessageToAll_FinalStep()
		local messageToMakePlayerSayInChat = listOfSetQuickChatStrings[whatMenuIsOpen][nameOfCurrentInput]
		local prefix = player.DisplayName
		local fullMessage_withPrefix = prefix..": "..messageToMakePlayerSayInChat

		SendQuickChatToAllOtherPlayers:FireAllClients(fullMessage_withPrefix)
	end
	
	---
	
	local function main()
		--print("-----------")
		-- allow input
		if not isValidInput() then
			warn("Invalid quick chat message tried to be sent.")
		else
			if isATimeoutInPlace() then
				local timeDiffSinceSpam = os.time() - player["quickChatSystem_TimeOfLastQuickChat_NumVal"].Value
				if timeDiffSinceSpam <= timeoutTimeForSpamming then
					-- tell player time left
					local metaData = "QuickChatSystem_TellPlayerNoSpamming"
					TellPlayerNoSpamming:FireClient(player,messageToPlayerIfTheyTryToSpam,metaData)
				else
					-- out of timeout now!
					--print("timeout over!")
					player["quickChatSystem_IsPlayerInTimeout_BoolVal"].Value = false
					player["quickChatSystem_TimeOfLastQuickChat_NumVal"].Value = os.time()
					sendMessageToAll_FinalStep()
				end
			else
				if isThisSpam() then
					--print("-- THIS IS SPAM!")					
					local metaData = "QuickChatSystem_TellPlayerNoSpamming"
					TellPlayerNoSpamming:FireClient(player,messageToPlayerIfTheyTryToSpam,metaData)
				else
					--print("THIS SHOULD BE WORKING!!!")
					sendMessageToAll_FinalStep()
				end
			end
		end
	end
	---
	
	main()
end)

UpdateUI.OnServerEvent:Connect(function(player,whatMenuIsOpen)
	if listOfSetQuickChatStrings[whatMenuIsOpen] then
		local menuToSentBack = listOfSetQuickChatStrings[whatMenuIsOpen]
		UpdateUI:FireClient(player,menuToSentBack)
	else
		warn("Invalid quick chat menu type tried to be sent.")
	end	
end)

Note that if you just use the code, and don’t use the file, you’ll also need RemoteEvents to connect the scripts. The list for quick chat can be found in the Server script.


Update Log for Resource

3/12/2025:

  • Added Fade in/out Animations
  • Added anti-spamming measures
  • Fixed issue where the UI would close prematurely/too early

3/11/2025:

  • Initial release
  • Security issue as a player can change the message locally that is shown to all

*Note: This uses a CanvasGroup for ease of transparency animation. Feel free to check out the documentation for its drawbacks.

  • Reasoning: (image, text, frame, etc. would otherwise have to be catered to for Tweens in a for loop. This isn’t ideal if devs want to add some UI.).

Feel free to suggest features or report issues you find!

8 Likes

A server check for the string would be nice, so people can’t fire the event with whatever they want, lol.

3 Likes

Edit: nvm, the thing they’d be editing would only show on the client, not everyone else. The server check would be for what other users see. My bad.

old reply

If I put the list on the server, and check from there, I think that’d help. But, couldn’t people just change the message from generalChannel:DisplaySystemMessage(fullMessage_withPrefix)? That line of code can only be run on the client.

If they have access to the client, they access fullMessage_withPrefix no? And thus, I’d say any and all measures to stop tampering on the server before that don’t matter. And, you can’t do anything I see after that.

1 Like

This issue should now be fixed. A warning will be thrown if it’s invalid. Thanks for pointing it out!

2 Likes

Here’s a suggestion. Use shoulder or trigger buttons to switch pages

2 Likes