I have written a LocalScript (MessageController) for a story-type Roblox game called Police Raid. This script controls the flow of the story by changing the UI messages and triggering events.
-- This script controls the progress of the messages! (It's the GOAT)
local message = script.Parent.Message
-- Wait for character to load
local player = game:GetService("Players").LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
-- Change message
task.wait(7)
message.Text = "Back there is the panic room. If there's any trouble, click the big red button and get inside. We're just going to hole up in here until someone comes to help us."
-- Change message
task.wait(7)
message.Text = "[static] This is the Robloxian Police Department, Barricaded Supsects Division. We have a warrant for your arrest on counts of [static]..."
message.TextColor3 = BrickColor.Red().Color
script["Heli Sound"]:Play()
-- Change message
task.wait(5)
message.Text = "Uh oh..."
message.TextColor3 = BrickColor.Black().Color
-- Change message
task.wait(2)
message.Text = "If you don't come out and surrender now, we will raid the premises!"
message.TextColor3 = BrickColor.Red().Color
-- Change message
task.wait(3)
message.Text = "Get to the panic room! NOW!!!"
-- Enable panic room
game:GetService("Workspace").Button.Part.ClickDetector.MaxActivationDistance = 32
message.TextColor3 = BrickColor.Black().Color
game:GetService("Workspace").TouchDetector.Touched:Connect(function(otherPart: BasePart)
-- In the panic room
task.wait(1)
script["Explosion"]:Play()
-- Change message
task.wait(3)
message.Text = "They're raiding the house... we should be safe in here."
script["Loud Explosion"]:Play()
-- Change message
task.wait(2)
message.Text = "They're in the panic room!"
message.TextColor3 = BrickColor.Red().Color
script["Extra Loud Explosion"]:Play()
-- Change message
task.wait(2)
message.Text = "I don't think we're as safe as I thought. Grab that rifle from the floor and get to the basement, QUICK!"
message.TextColor3 = BrickColor.Black().Color
game:GetService("Workspace")["Panic Door"].Fire.Enabled = true
game:GetService("Workspace")["Panic Panel"].CanCollide = false
-- Change message
game:GetService("Workspace")["Basement Detector"].Touched:Connect(function(otherPart: BasePart)
task.wait(2)
message.Text = "Okay... this basement was designed to survive a raid, so we should be okay."
-- Change message
task.wait(7)
script["Extra Loud Explosion"]:Play()
game:GetService("Workspace")["Basement Wall"].Transparency = 1
message.Text = "Put your hands up!"
message.TextColor3 = BrickColor.Red().Color
-- Change message
task.wait(2)
message.Text = "Get down, part of the wall is still up!"
message.TextColor3 = BrickColor.Black().Color
local counter = 10
task.wait(1)
for i = counter, 0, -1 do
task.wait(1)
script.Gunfire:Play()
-- Damage player
-- Check if they are in a safe area
local safezone = game:GetService("Workspace")["Wall Parts"].Safezone
safezone.Touched:Connect(function() end) -- TouchInterest
local function CheckIfPlayerIsInArea(Part,Character)
local touching = Part:GetTouchingParts()
for i=1,#touching do
if touching[i] == Character.HumanoidRootPart then
return true
end
end
return false
end
if not CheckIfPlayerIsInArea(safezone, game:GetService("Players").LocalPlayer.Character) then
--game:GetService("ReplicatedStorage").DamagePlayer:FireServer(10)
game:GetService("Players").LocalPlayer.Character.Humanoid.Health -= 1
end
end
-- Change message
message.Text = "The Robloxian Freedom Fighters are here! Get into their helicopter and escape to headquarters!"
game:GetService("Workspace")["Basement Wall"].CanCollide = false
local fighters = game:GetService("Workspace")["Freedom Fighters"]
fighters["RFF-Beta-31"]:MoveTo(Vector3.new(-100.242, 3.003, 39.393))
fighters["RFF-Alpha-0"]:MoveTo(Vector3.new(-103.259, 3.003, 31.674))
fighters["RFF-Epsilon-97"]:PivotTo(CFrame.new(-90.501, 9.305, 40.968))
fighters["RFF-Sigma-69"]:MoveTo(Vector3.new(-98.693, 3.003, 24.884))
fighters["RFF-Gamma-3"]:MoveTo(Vector3.new(-122.102, 3.003, 25.833))
fighters["RFF-Foxtrot-11"]:MoveTo(Vector3.new(-96.868, 3.003, 48.219))
fighters["RFF-Delta-4"]:MoveTo(Vector3.new(-113.647, 3.003, 32.763))
game:GetService("Workspace")["RFF Heli"]:PivotTo(CFrame.new(-105.629, 3.5, 49.35))
local ChatService = game:GetService("Chat")
local talkpart = game:GetService("Workspace")["BSD Commander Parker"].Handle
do
ChatService:Chat(talkpart, "It's an ambush!")
end
print("Triggered")
task.wait(1)
script["Machine Gun Fire"]:Play()
end)
end)
The problem is, "Triggered" seems to be printed to the console 528 times, and the "Gunfire" sound is played on top of itself very rapidly. Parker the NPC says "It's an ambush!" over and over in chat bubbles above his head. The Freedom Fighters and the RFF Heli are moved so much that they end up disappearing, and the game lags due to so much rapid code execution. I have no idea why the code would execute so much. The part with the problem seems to be:
local counter = 10
task.wait(1)
for i = counter, 0, -1 do
task.wait(1)
script.Gunfire:Play()
-- Damage player
-- Check if they are in a safe area
local safezone = game:GetService("Workspace")["Wall Parts"].Safezone
safezone.Touched:Connect(function() end) -- TouchInterest
local function CheckIfPlayerIsInArea(Part,Character)
local touching = Part:GetTouchingParts()
for i=1,#touching do
if touching[i] == Character.HumanoidRootPart then
return true
end
end
return false
end
if not CheckIfPlayerIsInArea(safezone, game:GetService("Players").LocalPlayer.Character) then
--game:GetService("ReplicatedStorage").DamagePlayer:FireServer(10)
game:GetService("Players").LocalPlayer.Character.Humanoid.Health -= 1
end
end
-- Change message
message.Text = "The Robloxian Freedom Fighters are here! Get into their helicopter and escape to headquarters!"
game:GetService("Workspace")["Basement Wall"].CanCollide = false
local fighters = game:GetService("Workspace")["Freedom Fighters"]
fighters["RFF-Beta-31"]:MoveTo(Vector3.new(-100.242, 3.003, 39.393))
fighters["RFF-Alpha-0"]:MoveTo(Vector3.new(-103.259, 3.003, 31.674))
fighters["RFF-Epsilon-97"]:PivotTo(CFrame.new(-90.501, 9.305, 40.968))
fighters["RFF-Sigma-69"]:MoveTo(Vector3.new(-98.693, 3.003, 24.884))
fighters["RFF-Gamma-3"]:MoveTo(Vector3.new(-122.102, 3.003, 25.833))
fighters["RFF-Foxtrot-11"]:MoveTo(Vector3.new(-96.868, 3.003, 48.219))
fighters["RFF-Delta-4"]:MoveTo(Vector3.new(-113.647, 3.003, 32.763))
game:GetService("Workspace")["RFF Heli"]:PivotTo(CFrame.new(-105.629, 3.5, 49.35))
local ChatService = game:GetService("Chat")
local talkpart = game:GetService("Workspace")["BSD Commander Parker"].Handle
do
ChatService:Chat(talkpart, "It's an ambush!")
end
print("Triggered")
task.wait(1)
script["Machine Gun Fire"]:Play()
Sorry if this is a lot of code or if it's badly formatted.
I have attempted to debug the code by printing the text "Triggered" and have looked around the script for anything that may be causing the infinite looping. I have also tried adding a task.wait(math.huge)
to the end of the script to prevent it from executing multiple times (above the end
statements), but this did not affect the looping. Interestingly, Machine Gun Fire
(a looped sound) does not appear to play more than once. If it's helpful, the LocalScript is parented to a ScreenGui, which is parented to StarterGui.
I am unable to post this question on the Roblox Devforum due to a lack of reputation, since I have been browsing the site without being logged in for a long time.
The issue that you are facing is that you are connecting listeners to events and never disconnecting them, or debouncing events that fire multiple times. A common problem, especially with the Touched
event is that it can fire when anything in the Workspace touches it, and also fire multiple times for the same character model. If your hand and foot both touch a Part, the event will fire for both of those parts.
Aside from that, a lot of your game logic should happen in a Script, that way, when sounds are played or objects are moved, hidden, or destroyed, they should happen for everyone at the same time.
The only thing that needs to happen in a LocalScript is where you display messages. So let's set up a simple way for the server to push messages to the UI.
So follow these steps :
DisplayMessage
-- import services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- find the UI elements
local lblMessage = script.Parent.Message
-- find the RemoteEvent telling us that the server has a message for us
local DisplayMessage = ReplicatedStorage.DisplayMessage
-- display the messages when the server sends them to us
DisplayMessage.OnClientEvent:Connect(function(message : string, color : Color3)
lblMessage.Text = message
lblMessage.Color3 = color
end)
debounceTouch
. We'll use this to prevent a user from triggering a Touch event multiple times when their Character Model touches a part.local Players = game:GetService("Players")
-- Debounce a function so that it only fires once per player touch event
local function debounceTouch(part : BasePart, onPlayerTouched : (Player)->(), onPlayerTouchEnded : (Player)->()?)
-- keep track of which players are touching the part
local playersTouching : { [Player] : number } = {}
local touchedConnection = part.Touched:Connect(function(otherPart : BasePart)
local player = Players:GetPlayerFromCharacter(otherPart.Parent)
if not player then
return
end
if not playersTouching[player] then
-- a player has begun touching!
onPlayerTouched(player)
playersTouching[player] = 1
else
playersTouching[player] += 1
end
end)
local touchEndedConnection = part.TouchEnded:Connect(function(otherPart : BasePart)
local player = Players:GetPlayerFromCharacter(otherPart.Parent)
if not player then
return
end
-- when a player stops touching, decrement the counter so that we can clear the debounce flag
if playersTouching[player] then
playersTouching[player] -= 1
if (playersTouching[player] <= 0) then
playersTouching[player] = nil
-- the player has stopped touching, fire the callback if provided
if onPlayerTouchEnded then
onPlayerTouchEnded(player)
end
end
end
end)
-- return an object that functions like an RbxScriptConnection token so that we can disconnect it when it is no longer relevant
return {
Connected = (touchedConnection.Connected or touchEndedConnection.Connected),
Disconnect = function()
touchedConnection:Disconnect()
touchEndedConnection:Disconnect()
end,
}
end
return debounceTouch
-- import services
local ChatService = game:GetService("Chat")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")
-- import helper functions
local debounceTouch = require(ReplicatedStorage.debounceTouch)
-- create a helper function to send the messages to all players
local DisplayMessage = ReplicatedStorage.DisplayMessage
local function displayMessageWithDelay(delay : number, message : string, color : Color3?)
task.wait(delay)
DisplayMessage:FireAllClients(message, color)
end
-- Wait for any player to load into the game
if #Players.GetPlayers() == 0 then
Players.PlayerAdded:Wait()
end
-- define some values
local SAFEZONE_DAMAGE_PER_SECOND = 10
-- write down all the messages at the top
local messages = {
"Back there is the panic room. If there's any trouble, click the big red button and get inside. We're just going to hole up in here until someone comes to help us.",
"[static] This is the Robloxian Police Department, Barricaded Suspects Division. We have a warrant for your arrest on counts of [static]...",
"Uh oh...",
"If you don't come out and surrender now, we will raid the premises!",
"Get to the panic room! NOW!!!",
"They're raiding the house... we should be safe in here.",
"They're in the panic room!",
"I don't think we're as safe as I thought. Grab that rifle from the floor and get to the basement, QUICK!",
"Okay... this basement was designed to survive a raid, so we should be okay.",
"Put your hands up!",
"Get down, part of the wall is still up!",
"The Robloxian Freedom Fighters are here! Get into their helicopter and escape to headquarters!",
}
local colors = {
black = BrickColor.Black().Color,
red = BrickColor.Red().Color,
}
-- load some workspace elements
local ButtonClickDetector = Workspace.Button.Part.ClickDetector
local TouchDetector = Workspace.TouchDetector
local PanicDoor = Workspace["Panic Door"]
local BasementDetector = Workspace["Basement Detector"]
local BasementWall = Workspace["Basement Wall"]
local safezone = Workspace["Wall Parts"].Safezone
local fighters = Workspace["Freedom Fighters"]
local helicopter = Workspace["RFF Heli"]
local talkPart = Workspace["BSD Commander Parker"].Handle
-- start playing the game
displayMessageWithDelay(7, messages[1])
displayMessageWithDelay(7, messages[2], colors.red)
script["Heli Sound"]:Play()
displayMessageWithDelay(5, messages[3], colors.black)
displayMessageWithDelay(2, messages[4], colors.red)
displayMessageWithDelay(3, messages[5])
-- Enable panic room
ClickDetector.MaxActivationDistance = 32
local touchedConnection
touchedConnection = TouchDetector.Touched:Connect(function(otherPart: BasePart)
-- make sure that this only fires if a player touches it
if not Players:GetPlayerFromCharacter(otherPart.Parent) then
return
end
-- disconnect the connection so that it doesn't fire again
touchedConnection:Disconnect()
-- In the panic room
task.wait(1)
script["Explosion"]:Play()
displayMessageWithDelay(3, messages[6], colors.black)
script["Loud Explosion"]:Play()
displayMessageWithDelay(2, messages[7], colors.red)
script["Extra Loud Explosion"]:Play()
displayMessageWithDelay(2, messages[8], colors.black)
-- set the door on fire
PanicDoor.Fire.Enabled = true
PanicDoor.CanCollide = false
local basementTouchedConnection
basementTouchedConnection = BasementDetector.Touched:Connect(function(otherPart: BasePart)
-- make sure that this only fires if a player touches it
if not Players:GetPlayerFromCharacter(otherPart.Parent) then
return
end
-- disconnect the connection so it only fires once
basementTouchedConnection:Disconnect()
-- start playing the next set of messages
displayMessageWithDelay(2, messages[9])
displayMessageWithDelay(7, messages[10], colors.red)
-- explode the basement walls
script["Extra Loud Explosion"]:Play()
BasementWall.Transparency = 1
displayMessageWithDelay(2, messages[11], colors.black)
-- create a list of players currently touching the safezone
local playersInSafezone = {}
local playerList = Players:GetPlayers()
for _, player in ipairs(playerList) do
playersInSafezone[player] = false
end
local safezoneTouchConnection = debounceTouch(safezone, function(player)
-- when they touch the safezone, mark them as safe
playersInSafezone[player] = true
end, function(player)
-- when they stop touching the safezone, mark them as unsafe
playersInSafezone[player] = false
end)
task.wait(1)
for i = 10, 0, -1 do
task.wait(1)
script.Gunfire:Play()
-- Check if they are in a safe area
for player, isSafe in pairs(playersInSafezone) do
if not isSafe then
-- Damage player
player.Character.Humanoid:TakeDamage(SAFEZONE_DAMAGE_PER_SECOND)
--ReplicatedStorage.DamagePlayer:FireServer(10)
end
end
end
-- clean up the safezone events
safezoneTouchConnection:Disconnect()
playersInSafezone = nil
-- Change message
displayMessageWithDelay(0, messages[12])
BasementWall.CanCollide = false
-- move the fighters into place
fighters["RFF-Beta-31"]:MoveTo(Vector3.new(-100.242, 3.003, 39.393))
fighters["RFF-Alpha-0"]:MoveTo(Vector3.new(-103.259, 3.003, 31.674))
fighters["RFF-Epsilon-97"]:PivotTo(CFrame.new(-90.501, 9.305, 40.968))
fighters["RFF-Sigma-69"]:MoveTo(Vector3.new(-98.693, 3.003, 24.884))
fighters["RFF-Gamma-3"]:MoveTo(Vector3.new(-122.102, 3.003, 25.833))
fighters["RFF-Foxtrot-11"]:MoveTo(Vector3.new(-96.868, 3.003, 48.219))
fighters["RFF-Delta-4"]:MoveTo(Vector3.new(-113.647, 3.003, 32.763))
helicopter:PivotTo(CFrame.new(-105.629, 3.5, 49.35))
ChatService:Chat(talkPart, "It's an ambush!")
print("Triggered")
task.wait(1)
script["Machine Gun Fire"]:Play()
end)
end)
You'll notice that I've changed how you are damaging the player, and how the safezone works. This way we can quickly check who is touching the safezone every time without needing to ask the engine to do lookups of parts to players.
The only other thing I really did was move all of the game messages to a list at the top, this wasn't really necessary, but I thought it would make it easier to see the game logic instead of reading through the messages inside the body of the script. This is my preference, and really isn't necessary for the functioning of your code.
Hopefully this helps.