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.
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)
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
---------------------// 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.
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:
Touched to initiate
.TouchEnded or distance checking to handle “leaving”
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?