Issue with .Touched script not always working?

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.

I’m guessing…

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

Let me know if there are any problems.

1 Like

instead of

: WaitForChild ("Hum")

Do

:FindFirstChild("Humanoid")

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.

1 Like

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")
1 Like

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
1 Like

@Y_VRN @geometricalC2123 @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.

2 Likes

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.

Under what circumstances would a leaderstats folder and subsequent values be parented to the player long after they have joined?

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.