In my obby game, I recently updated the way .Touched events get handled. Now the server just loops through all checkpoints in the game to see if they get touched by a player. But when I published it players started reporting that the checkpoints sometimes don’t work. The code that should follow when a player touches the checkpoint is just not running at all. It’s really hard to find out what’s wrong, since most of the time the checkpoints work and I can’t replicate the bug.
The game lost 500 concurrent players because of this, so I really need to fix it.
↓ here is the code, I tried to explain everything in comments but ask me if there’s something unclear.
local Checkpoints = workspace:WaitForChild("Checkpoints") --reference to a folder with all the checkpoints
for _,checkpoint in pairs(Checkpoints:GetChildren()) do --looping through all the checkpoints
local Debounce = false --a debounce to prevent players to get the checkpoint reward twice
checkpoint.Touched:Connect(function(touched) --when a checkpoint gets touched (touched = part that touched the checkpoint)
if Debounce == false then --if debounce is false (then it's okay to run this code)
Debounce = true --setting debounce to true for now
if touched.Parent:FindFirstChild("Humanoid") then --if the touched part is a player
local Player = game.Players:GetPlayerFromCharacter(touched.Parent) --referencing the player
local Humanoid = touched.Parent:WaitForChild("Humanoid") --referencing the player's humanoid
if tonumber(checkpoint.Name) == Player.leaderstats.Stage.Value + 1 and Humanoid.Health ~= 0 then --if it's the correct checkpoint and if player is still alive
Player.leaderstats.Stage.Value = tonumber(checkpoint.Name) --updating the player's stats to the new checkpoint
Player.RespawnLocation = checkpoint --setting the checkpoint as the player's respawn location
--here follows some other code here that gives the player a reward
end
end
wait(0.1) --a small delay before putting debounce to false again
Debounce = false
end
end)
end
I’m thinking maybe it has to do with the debounce, which might be activated by another part, so it will be ignored when the players touch the checkpoint. But debounces are used so much and I never heard anybody had troubles with them. So why is this script not working?
If someone could help me clear up this mistery that would be greatly appreciated. Thanks in advance.
Why is there a debounce in the first place? A player could walk over the checkpoint, trigger the debounce, and within 0.1 seconds, another player could walk over the checkpoint, and when they die, they don’t get respawned at the checkpoint.
The way I see this is Accessories’ handle parts could interfere with the Script, because the handles’ parents are Accessories. Also, I wouldn’t want to set Debounce to true and wait for 0.1 seconds before properly checking what was touched.
Also, instead of checking the part’s parent if it is a player’s character, check if it is a descendant of the player’s character.
And lastly, the line…
local Humanoid = touched.Parent:WaitForChild("Humanoid")
… is probably not needed. The player’s characters may have already spawned with their Humanoid objects, so waiting isn’t needed.
So, try this:
local Players = game:GetService("Players") -- Get Players
local Checkpoints = workspace:WaitForChild("Checkpoints")
for _,checkpoint in pairs(Checkpoints:GetChildren()) do
local Debounce = false
checkpoint.Touched:Connect(function(touched)
if Debounce == false then
if touched.Parent:FindFirstChild("Humanoid") then
-- Check if it is a descendant of one of the players. Otherwise, skip.
local Player
for _, v in pairs(Players:GetChildren()) do
if touched:IsDescendantOf(v.Character) then
Player = v
end
end
if not Player then
return -- Cancel if Player isn't found.
end
local Humanoid = Player:FindFirstChild("Humanoid") -- Don't wait for Humanoid if they have already spawned.
if not Humanoid then
return -- Cancel if Humanoid doesn't exist.
end
-- Set Debounce after everything has passed.
Debounce = true
if tonumber(checkpoint.Name) == Player.leaderstats.Stage.Value + 1 and Humanoid.Health ~= 0 then
Player.leaderstats.Stage.Value = tonumber(checkpoint.Name)
Player.RespawnLocation = checkpoint
end
wait(0.1) -- Wait just before setting Debounce to false again.
end
Debounce = false
end
end)
end
If you waitforchild,it will yield the script which means pause , so if an unanchored object touches it it will yield, so to prevent that : FindFirstChild should be the solution.
The problem is WaitForChild will keep looking for ANYTHING named Humanoid inside touched.Parent. Also, WaitForChild will not stop looking until the “Humanoid” instance is found, so for example, if the Player doesn’t have a Humanoid, (Which is basically impossible), then it will keep looking and pause everything beyond it until Humanoid was found. Try using:
local Humanoid = touched.Parent:FindFirstChild("Humanoid")
The WaitForChild() is fine, he should just have it inside a conditional which checks if the player/character is valid.
local Checkpoints = workspace:WaitForChild("Checkpoints")
local Debounce = false
local Players = game:GetService("Players")
for _, checkpoint in ipairs(Checkpoints:GetChildren()) do
checkpoint.Touched:Connect(function(touched)
if Debounce then
return
end
if touched.Parent:FindFirstChild("Humanoid") then
Debounce = true
local Character = touched.Parent
local Player = Players:GetPlayerFromCharacter(touched.Parent)
if Player then
local Humanoid = Character:WaitForChild("Humanoid")
if tonumber(checkpoint.Name) == Player.leaderstats.Stage.Value + 1 and Humanoid.Health ~= 0 then
Player.leaderstats.Stage.Value = tonumber(checkpoint.Name) --updating the player's stats to the new checkpoint
Player.RespawnLocation = checkpoint
end
end
end
task.wait(0.5)
Debounce = false
end)
end
@Y_VRN@regexman@CommanderRanking thank you for all your replies. Part of the problem has been fixed now but there’s still a few things unclear to me.
The WaitForChild("Humanoid") only runs after the Humanoid has already been found by FindFirstChild("Humanoid"). So that’s not an issue except for very rare cases where the player leaves just while touching a checkpoint then it might yield.
But maybe those few cases are indeed what happened, since most of the time the checkpoints worked fine and then randomly one doesn’t.
If this is the case and there is no Humanoid anymore, wouldn’t :FindFirstChild() just give another error like “attempt to call nil”?
Thanks for your reply. And yeah I don’t think the WaitForChild() on it’s own is that much of a problem if there would be a conditional. But actually isn’t there already a conditional which checks if the player is valid?
if touched.Parent:FindFirstChild("Humanoid") then
↑ This line already checks if the part that touched the checkpoint is part of a player
So what is the use of checking it again? ↓
if Player then
Maybe there’s a difference, I’m guessing maybe the player-model despawns faster than it’s character in the workspace. But I’m not sure how exactly it works, could you clarify on this or just link a developerhub article. Thanks in advance.
It’s possible if you have NPC’s in the game which would also have the Humanoid instance inside of them to trigger the event to fire, they would not pass the player check however. Alternatively no WaitForChild() is possible.
local Checkpoints = workspace:WaitForChild("Checkpoints")
local Debounce = false
local Players = game:GetService("Players")
for _, checkpoint in ipairs(Checkpoints:GetChildren()) do
checkpoint.Touched:Connect(function(touched)
if Debounce then
return
end
if touched.Parent:FindFirstChild("Humanoid") then
Debounce = true
local Humanoid = touched.Parent:FindFirstChild("Humanoid")
local Character = touched.Parent
local Player = Players:GetPlayerFromCharacter(touched.Parent)
if Player then
if tonumber(checkpoint.Name) == Player.leaderstats.Stage.Value + 1 and Humanoid.Health ~= 0 then
Player.leaderstats.Stage.Value = tonumber(checkpoint.Name) --updating the player's stats to the new checkpoint
Player.RespawnLocation = checkpoint
end
end
end
task.wait(0.5)
Debounce = false
end)
end
The issue is related to the if statement raising an error causing Debounce to forever stay false, it may be due players which take longer than usual to load, I tried making your code as error proof I was able to:
local Players = game:GetService("Players")
local Checkpoints = workspace:WaitForChild("Checkpoints")
for _,checkpoint in pairs(Checkpoints:GetChildren()) do
local Debounce = false
checkpoint.Touched:Connect(function(touched)
if not Debounce then
Debounce = true
local character = touched.Parent
local humanoid = character:FindFirstChildWhichIsA("Humanoid")
if not humanoid then Debounce = false return end
local player = Players:GetPlayerFromCharacter(character)
if not humanoid then Debounce = false return end
local leaderstats = player:FindFirstChild("leaderstats")
if not leaderstats then Debounce = false return warn("leaderstats not found/loaded") end
local stage = leaderstats:FindFirstChild("Stage")
if not stage then Debounce = false return warn("stages not found/loaded") end
local level = tonumber(checkpoint.Name)
if level == stage.Value+1 and humanoid.Health > 0 then
stage.Value = level
player.RespawnLocation = checkpoint
end
task.wait(0.1)
Debounce = false
end
end)
end
PS: to avoid all this and to ensure everything runs you can wrap your if statement inside a pcall, although it might be a more expensive solution.
You realise the if statement can’t raise an error right?
if Instance:FindFirstChild() then will either return the found instance which is truthy or nil which is falsy, unless the instance itself through which FindFirstChild() is called doesn’t exist the conditional statement will not error.
In this case “touched.Parent” will always be a reference to an instance because everything is a descendant of the workspace.
The if statement itself wont cause an error, but the code wrapped in it will in some occasions. There is a chance the player character loads before their data causing something in the path of Players.leaderstats.Stage to be nil, which will then raise an error related to indexing nil breaking the script and preventing the Debounce from being set to false.
Then you could simply add WaitForChild() commands on the leaderstats folder & the Stage stat inside of it, although by the time a player is able to touch a new checkpoint those should have long been loaded.
I had already fixed the possible debounce issues in my implementations & I wouldn’t handle debouncing in the way you have.
checkpoint.Touched:Connect(function(touched)
if not Debounce then
Debounce = true
In this case any touching BasePart will cause the debounce to be activated.
If WaitForChild yields it will also cause the same issue. Also we have no way to “prevent” the amount of time it will take for user data to load, cause in some occasions datastores may be down/or slow(And we can’t wait in general cause other users will be unable to use the checkpoint).
The leaderstats folder and Stage stat are likely parented to the player upon them joining in which case they wouldn’t raise an infinite yield warning. Relying on values to be retrieved from a DataStore isn’t relevant (worst case a player has to wait a little while for his/her stats to load) but the script will still have all the necessary references to instances to perform as necessary.
This is something we have no way to prove, without having access to how the data is being loaded, therefore it’s better not to assume it. Also it’s better not to discuss Datastores as I consider it off-topic.
@Forummer@NyrionDev the leaderstats.Stage.Value is set to 1 by default before it’s parented to the player. So even if the data takes some time to load it’ll never be nil.
There’s our confirmation, thank you, I’ve also slightly modified the earlier version.
local Checkpoints = workspace:WaitForChild("Checkpoints")
local Debounce = false
local Players = game:GetService("Players")
for _, checkpoint in ipairs(Checkpoints:GetChildren()) do
checkpoint.Touched:Connect(function(touched)
if Debounce then
return
end
if touched.Parent:FindFirstChild("Humanoid") then
Debounce = true
local Humanoid = touched.Parent:FindFirstChild("Humanoid")
local Character = touched.Parent
local Player = Players:GetPlayerFromCharacter(touched.Parent)
if Player and Character and Humanoid then
local stageNum = tonumber(checkpoint.Name)
local leaderstats = Player:WaitForChild("leaderstats")
if leaderstats then
local Stage = leaderstats:WaitForChild("Stage")
if Stage then
if stageNum == Stage.Value + 1 and Humanoid.Health ~= 0 then
Stage.Value = stageNum
Player.RespawnLocation = checkpoint
end
end
end
end
end
task.wait(0.5)
Debounce = false
end)
end
A lot of unnecessary waits and checks but it’ll essentially ensure everything in each line exists before executing the next.
Then the issue must be related to WaitForChild("Humanoid") causing an infinity yield. As said above my solution is a bit more over-complicated than @Forummer but it will ensure none of this happen. In the worst case scenario it’s better to pcall anything that might cause Debounce to never reset.