How to detect Player Touched event only once?

Issue:

I am making a Group Reward system where the player gets an item if they are in the group. They can get the item by touching a part. So I am firing a Remote Event which checks whether a person is in the group or not and if they are not, then it fires back to the client and then client script shows a UI pop-up.

I want the ClaimButton part to detect a singular touch and that’s it. If the player is not in the group, they will be shown a UI frame, where the player has to press on Okay button on the UI to make it disappear. If the player walks off the ClaimButton part and walks back on it without being in the group, the UI is shown again.

The main issue is that walking on it correctly popups the UI but then after 2 seconds, the UI disappears and instantly reappears even though I am standing still on the button.


Here’s the code:

Code
local touchedPlayers = {}

ClaimButton.Touched:Connect(function(hitPart)
	local player = Players:GetPlayerFromCharacter(hitPart.Parent)

	if not player then return end
	if touchedPlayers[player] then return end

	print("TOUCHED:", touchedPlayers)
	touchedPlayers[player] = true
	Remotes.GroupReward:FireServer()

	-- Reset this player after a delay
	task.spawn(function()
		task.wait(2)
		touchedPlayers[player] = nil
		print("RESET:", touchedPlayers)
	end)
end)

What I have tried already:

  • I have already tried setting ClaimButton.CanTouch = false
  • ClaimButton.TouchedEnded but does not seem to work.
  • Debounce value, not working

Images/Video:

1 Like

Add a debounce so local db = false then in the script so if db == true then rest of code. Then db = false and after the end of the code do

 while task.wait(1) do
If db == false then
db = true
end

Something like that

Instead of

touchedPlayers[player] = true

Use

table.insert(touchedPlayers, player)

then replace

if touchedPlayers[player]

with

if table.find(touchedPlayers, player)

Because it’s touching and untouching many times in a row as you move around on the touch trigger, that’s why it’s disappearing and reappearing. If you move, it’s a touch; if you stop moving, it’s a touch ended … and so on.

You never said what the reward was. You never said why you want the UI to stay open until they leave the touch trigger.

Let’s say the reward is a hat and there’s no reason to keep the UI open longer than seeing you got the reward.

pseudo code

They touched the trigger
 if they have the reward then just return
 if they touch the trigger grouped then they get the reward 
 fire a remote to display they received the reward
 --it holds for a few seconds and closes.

You should start by removing the touchedPlayers table, since this is a local script, meaning it should only handle the local player, not others. Otherwise, players could claim 1 * NumberOfPlayers rewards every 2 seconds.

Additionally, you can remove the task.spawn. Each signal connection already runs the associated function in a new thread, so using a wait inside it will only yield that thread without affecting the rest of the script. Since you don’t do anything after the wait time, there’s no need to create an additional spawn thread.

Here is the new version

local touchCooldown = tick()

ClaimButton.Touched:Connect(function(hitPart)
	if not Players:GetPlayerFromCharacter(hitPart.Parent) then return end
	if tick() - touchCooldown < 2 then return end

	touchCooldown = tick()
	Remotes.GroupReward:FireServer()
end)

Regarding the UI popup, there is no need to use FireClient at all. You can directly check on the client side whether the player is in the group, and only call FireServer if that’s the case. Of course, you should still keep that check on the server side to prevent exploits, just remove the FireClient call.

You should have a single script that handles the entire feature, including the touched event and the GUI popup. Here’s an example (bellow):

I used a loop to check whether the character is on the button because the touchEnded connection is buggy and unreliable.

local PlayerService = game:GetService("Players")

local player = PlayerService.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local root = character:WaitForChild("HumanoidRootPart", 60)

local touchCooldown = tick()
local isOnButton = false

local function SwitchPopupGui(IsInGroup)
	if IsInGroup == false and PopupGUI.Visible == false then
		PopupGUI.Visible = true
	elseif IsInGroup == true and PopupGUI.Visible == true then
		PopupGUI.Visible = false
	end
end

local function WaitForButtonLeave()
	repeat task.wait(0.2)
		if root and (root.Position - claimButton.Position).Magnitude > 20 then
			PopupGUI.Visible = false
			isOnButton = false
		end
	until isOnButton == false
	
	SwitchPopupGui(true)
end

local function ButtonTouched(hitPart)
	local touchedPlayer = PlayerService:GetPlayerFromCharacter(hitPart.Parent)

	if not touchedPlayer or touchedPlayer ~= player then return end
	if tick() - touchCooldown < 2 then return end
	
	local IsInGroup = player:IsInGroup(00000000)
	
	SwitchPopupGui(IsInGroup)
	
	if IsInGroup then
		touchCooldown = tick()
		Remotes.GroupReward:FireServer()
	end
	
	if isOnButton == false then
		isOnButton = true
		WaitForButtonLeave()
	end
end

local function UpdateCharacter(newCharacter)
	character = newCharacter
	root = character:WaitForChild("HumanoidRootPart", 60)
	isOnButton = false
end

claimButton.Touched:Connect(ButtonTouched)
player.CharacterAdded:Connect(UpdateCharacter)

use this ZonePlus v3.2.0

So if I’m understanding this correctly you want the ui to be visible only while the player is touching a part. (And maybe have a delay after leaving the part?)

Without delay:

local ui = script.Parent --Set to your ui you want to enable/disable

local player = game.Players.LocalPlayer

local touches = 0

local ClaimButton = workspace.TouchPart --Your claim button

ClaimButton.Touched:Connect(function(hit)
	if hit.Parent == player.Character then --It's fine - no need to account for accessories
		--[[
			Whatever logic needs to be done here
			- for example: checking whether the player is in the group (player:IsInGroup(groupId))
		]]
		
		ui.Enabled = true --or ui.Visible = true depending on the instance type you're making visible
		
		touches += 1
	end
end)
ClaimButton.TouchEnded:Connect(function(hit)
	if hit.Parent == player.Character then --No need to account for accessories
		touches -= 1	
	end
	if touches == 0 then
		ui.Enabled = false --same here - ui.Visible = false
	end
end)

With delay:

local ui = script.Parent --Set to your ui you want to enable/disable

local player = game.Players.LocalPlayer

local ClaimButton = workspace.TouchPart --Your claim button

local touches = 0
local lastTouch = os.clock() --Or use tick() whatever you find better (both are fine in this case)

local function disableUI()
	--If it's been 2 seconds since the last touch, disable the UI
	--(Checking time because the player could have touched again while the function is delayed)
	if touches == 0 and os.clock() - lastTouch > 1.95 then	
		ui.Enabled = false
	end
end

ClaimButton.Touched:Connect(function(hit)
	if hit.Parent == player.Character then --It's fine - no need to account for accessories
		--[[
			Whatever logic needs to be done here
			- for example: checking whether the player is in the group (player:IsInGroup(groupId))
		]]
		
		ui.Enabled = true --or ui.Visible = true depending on the instance type you're making visible
		touches += 1
		lastTouch = os.clock()
	end
end)
ClaimButton.TouchEnded:Connect(function(hit)
	if hit.Parent == player.Character then --No need to account for accessories
		touches -= 1	
	end
	if touches == 0 then
		task.delay(2, disableUI)
	end
end)

I would also recommend using an invisible part with CanCollide set to false for the touch detections

Already tried, did not work :frowning:


That is just a different coding style, irrelevant to the solution.


Thank you I will give this a try


could u send the full code please also where is this script located

Full code:

Code
---------------------// SERVICES \\---------------------

local RS = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

----------------------// MODULES \\---------------------

local UIAnimator = require(RS.Modules.UIAnimator)

----------------------// REMOTES \\---------------------

local Remotes = RS:WaitForChild("Remotes")

------------------------// UI \\------------------------

local PlayerGui = Players.LocalPlayer:WaitForChild("PlayerGui")
local FramesGui: ScreenGui = PlayerGui:WaitForChild("Frames")

local NotInGroup: ImageLabel = FramesGui:WaitForChild("NotInGroup")
local OkayButton: ImageButton = NotInGroup.Okay

---------------------// REFERENCES \\--------------------

--local ClaimButtonPart: BasePart = workspace:WaitForChild("Lobby"):WaitForChild("GroupRewards"):WaitForChild("ClaimButton")
--local SGui: SurfaceGui = ClaimButtonPart:WaitForChild("SurfaceGui")
--local ClaimButton: ImageButton = SGui:WaitForChild("Claim!")

local FreeRewards: Model = workspace:WaitForChild("Lobby"):WaitForChild("FreeRewards")
local GroupRewardModel: Model = FreeRewards:WaitForChild("RewardModel")
local ClaimButton: BasePart = GroupRewardModel:WaitForChild("Button")

-----------------------// TOUCHED \\----------------------

local touches = 0

ClaimButton.Touched:Connect(function(hitPart)
	local player = Players:GetPlayerFromCharacter(hitPart.Parent)

	if not player then return end
	if touches > 0 then return end

	touches = 1
	print("TOUCHES:", touches)
	
	Remotes.GroupReward:FireServer()
end)

ClaimButton.TouchEnded:Connect(function(hitPart)
	local player = Players:GetPlayerFromCharacter(hitPart.Parent)

	if not player then return end
	touches = 0
	
	print("TOUCHES:", touches)
end)

------------------------// EVENT \\-----------------------

-- Player is not in group
Remotes.GroupReward.OnClientEvent:Connect(function(successful)
	if not successful then
		UIAnimator.FrameSlideUp(NotInGroup)
	end
end)

OkayButton.Activated:Connect(function()
	if NotInGroup.Visible == true then
		UIAnimator.FrameSlideDown(NotInGroup)
	end
end)

Was trying a different method but this is yielding the same result. The issue with Roblox’s Touch detection system is that even standing still is connecting the .Touched and .TouchEnded functions. Makes no sense.

i was gonna say try something like this inside the ui idk u might have to change the names bc i just put random but try something like this.

local runservice = game:GetService("RunService")
local Player = game.Players.LocalPlayer

local touchedPlayers = {}

local Remotes = game.ReplicatedStorage.Remotes -- ur remotes


runservice.RenderStepped:Connect(function()
	if Player.Character:FindFirstChild("Humanoid") then
		if Player.Character.Humanoid.Health ~= 0 then
			local TouchAvailable = {}
			for i,v in pairs(game.Workspace.ClaimButton:GetChildren()) do
				local mag = (v.Position-Player.Character:WaitForChild("HumanoidRootPart").Position).Magnitude
				if mag <= 5 then
					TouchAvailable[#TouchAvailable+1] = v.Name
					print("Touched")
				end
			end
			if #TouchAvailable == 1 then
				local ClaimButton = workspace.ClaimButton:FindFirstChild(TouchAvailable[1]) -- ur claim button
				Remotes.GroupReward:FireServer()
				print("Touched")
				script.Parent.theui:TweenPosition(UDim2.new(0.5, 0,0.5, 0),Enum.EasingDirection.In,Enum.EasingStyle.Linear,0.2)
			else
				script.Parent.theui:TweenPosition(UDim2.new(0.5, 0,-0.5, 0),Enum.EasingDirection.In,Enum.EasingStyle.Linear,0.2)
			end

		end
	end
end)

Ok so I have gone and decided to use RenderStepped to detect the player’s distance on/off the base.

RunService.RenderStepped:Connect(function()
	local char = player.Character
	if not char then return end

	local hrp = char:FindFirstChild("HumanoidRootPart")
	if not hrp then return end

	local dist = (hrp.Position - ClaimButton.Position).Magnitude
	local touchingNow = dist <= 5

	if touchingNow and not isTouching and not uiOpen then
		isTouching = true
		Remotes.GroupReward:FireServer()
		
	elseif not touchingNow and isTouching then
		isTouching = false
	end
end)

This seems to be working for now but I will look into more efficient and cleaner ways of handling this.

1 Like

Do touches+=1 and -=1 and remove if touches > 0 then return end
then the condition is touches>0 for ui enabling/disabling

doing

tablevariable[stuff] = other stuff

rather than

table.insert(tablevariable, stuff)

is actually really different (search it up if you have the time).. it’s better to just use table.insert for your use case, I use it to make sure my explosion systems only damages the same player once

The issue is classic: .Touched keeps firing even if the player is standing still, due to small physics jitters or overlapping body parts (e.g. limbs touching the part again). Your task.wait(2) reset opens up a window for re-triggers even while the player hasn’t moved.

Let me walk you through a robust fix using a combination of:

  1. Touched to initiate
  2. .TouchEnded or distance checking to handle “leaving”
  3. Tracking per-player debounce using Region3 or part proximity

Goal:

  • Show UI only once per contact
  • UI clears when player presses Okay
  • If they step off and back on, UI shows again
  • No repeated flickering while standing still

Best fix: use Touched, + track distance via RunService.Heartbeat

This version only resets the player’s debounce once they physically step off the part.


Updated code (SERVER + CLIENT version)

-- ServerScript (inside the Part or somewhere else)
local ClaimButton = script.Parent
local Players = game:GetService("Players")
local Remotes = game.ReplicatedStorage.Remotes -- assume you have GroupReward remote here

local touchingPlayers = {}

local function isPlayerTouchingPart(player)
	local char = player.Character
	if not char then return false end
	local hrp = char:FindFirstChild("HumanoidRootPart")
	if not hrp then return false end

	local dist = (ClaimButton.Position - hrp.Position).Magnitude
	return dist <= (ClaimButton.Size.X / 2 + 3) -- tweak the "+3" for forgiveness
end

game:GetService("RunService").Heartbeat:Connect(function()
	for player, _ in pairs(touchingPlayers) do
		if not isPlayerTouchingPart(player) then
			touchingPlayers[player] = nil
			print("Player left the ClaimButton:", player.Name)
		end
	end
end)

ClaimButton.Touched:Connect(function(hit)
	local player = Players:GetPlayerFromCharacter(hit.Parent)
	if not player then return end
	if touchingPlayers[player] then return end

	touchingPlayers[player] = true
	print("Player touched the ClaimButton:", player.Name)

	Remotes.GroupReward:FireServer(player)
end)

ClientScript (inside LocalScript inside StarterPlayerScripts or GUI)

-- LocalScript to handle GroupReward response and UI
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local Remotes = game.ReplicatedStorage.Remotes

local popup = script.Parent:WaitForChild("GroupPopupFrame") -- replace with your UI frame
popup.Visible = false

Remotes.GroupReward.OnClientEvent:Connect(function()
	popup.Visible = true
end)

popup.OkayButton.MouseButton1Click:Connect(function()
	popup.Visible = false
end)

Explanation:

  • touchingPlayers[player] only resets if the player physically moves away (distance check).
  • This prevents the UI from flickering while idle.
  • Touched is allowed to fire again only after they’ve left and come back.
  • The debounce is now based on position, not time.

Bonus tip:

If you want this to be more performant for large servers, use ZonePlus, a Roblox module for cleaner proximity detection.

Would you like a working Roblox model file or template for this system?

2 Likes

@KrimsonWoIf Bro did you use AI :skull:

Is the solution I provided working well?

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.