Tycoon button grants income boost multiple times in a millisecond

Hi all! I’m currently developing a tycoon game, but I’ve run into a slight issue: when a player touches a button, the script is supposed to spawn an object, update the players’ income, and disappear. The spawn works great and the objects do not duplicate, but for some odd reason the game will occasionally recognize multiple button presses in the literal millisecond it takes for the button to disappear, resulting in the player’s income value being increased by up to four times the button’s intended value.

Button.Touched:Connect(function(hit)
				local player = game.Players:GetPlayerFromCharacter(hit.Parent)
				if not player then return end
				if plot:GetAttribute('Owner') ~= player.UserId then return end
				
				local itemToUnlockId = Button:GetAttribute('UnlockId')
				if not itemToUnlockId then warn('You forgot to add an UnlockId attribute!') return
				end
				
				-- Check if player has enough money
				local Price = Button:GetAttribute('Price')

				if Price then
					if player.leaderstats.Cash.Value < Price then
						warn('You cannot afford this item!')
						return
					end
					
					player.leaderstats.Cash.Value -= Price
				end
				
				for _, item in TempItems:GetChildren() do
					if item:GetAttribute('Id') ~= itemToUnlockId then continue end
					
					local itemClone = item:Clone()
					
					local itemCFrame
					
					if itemClone:IsA('Model') then
						itemCFrame = itemClone:GetPivot() --Model
					elseif itemClone:IsA('BasePart') then
						itemCFrame = itemClone.CFrame --BasePlate (Part, Union, Mesh, etc.)
					end
					
					local relativeItemCFrame = TempPlot.CFrame:ToObjectSpace(itemCFrame)
					local worldCFrameOfNewPlot = plot.CFrame:ToWorldSpace(relativeItemCFrame)
					if itemClone:IsA('Model') then
						itemClone:PivotTo(worldCFrameOfNewPlot)
					elseif itemClone:IsA('BasePart') then
						itemClone.CFrame = worldCFrameOfNewPlot
					end
					
					-- Updates the player's income to Revenue attribute in button
					local Income = player.leaderstats.Income
					local Revenue = Button:GetAttribute('Revenue')
					Income.Value += Revenue
					
					itemClone.Parent = itemsFolder
					Button:Destroy()
					print('Button has been pressed by '..player.Name..' and their income has been increased to '..Income.Value..'!')
					
				end
			end)
1 Like

Rather than rely on the destruction of the button to disable repeated interaction, formally disconnect your callback from the Touched event:

local function onLivingPlayerTouched(part: BasePart, callback: (Player) -> ()): RBXScriptConnection
    return part.Touched:Connect(function(otherPart: BasePart)
        local character = otherPart.Parent :: Model
        
        local humanoid = character:FindFirstChildOfClass("Humanoid")
        if not humanoid or humanoid.Health == 0 then
            return
        end
        
        local player = Players:GetPlayerFromCharacter(character)
        if player then
            callback(player)
        end
    end)
end
local connection

connection = onLivingPlayerTouched(Button, function(player: Player)
   connection:Disconnect()

    -- Reward player.
end)

Just tried adding this to the script, and it seems to have had no change on the outcome. I put the local function close to the beginning of my script and the local connection right before the reward- is there a particular order these need to be introduced into the script in order for the formal disconnect to properly execute?

add a debounce to your touched event and disconnect like ziffix said

Try replacing :Connect() with :Once()

2 Likes

Hey! Is there a reason why your income logic is inside of the for loop? This could be causing the issue as each time the loop occurs income would increase. Let me know if removing the income logic from the for loop did fix the issue. You can change the :Connect to :Once and disconnect the connection but you are already destroying the part itself so these would not do anything. I am 99% sure its because your income logic is inside of the for loop.

By making it a :Once() system instead of a :Connect() system, it would make the buttons vulnerable to being pressed when a player who doesn’t own the plot or have enough cash walks over it. If that happens, the button can no longer be purchased and the game will not be able to move forward.

I tried relocating the income logic to a new location outside of the for loop, but that made no change to the outcome. The button is still vulnerable to being pressed multiple times before disappearing.

I added a debounce to the events, but it seems that it’s only had an effect on the objects- the buttons can now only spawn a single object rather than being able to spawn multiple simultaneously. The income issue is still occurring.

You removed the purchase logic outside of the for loop? And nothing changed? Can you send me the updated script?

Where did you put the debounce at exactly? The object spawning yields so it can create a race condition giving you cash multiple times

Blame deferred signals!!!
Immediate mode superior, deferred mode inferior.

1 Like

Add an if statement and a boolean to check if the player already bought the button. Please let me know if this helps! :happy3:


Code
local Bought = false
local Running = false -- You may not need this, just in case.

Button.Touched:Connect(function(hit)
				if Bought or Running then return end
				Running = true
				
				local player = game.Players:GetPlayerFromCharacter(hit.Parent)
				if not player then return end
				if plot:GetAttribute('Owner') ~= player.UserId then return end
				
				local itemToUnlockId = Button:GetAttribute('UnlockId')
				if not itemToUnlockId then warn('You forgot to add an UnlockId attribute!') return
				end
				
				-- Check if player has enough money
				local Price = Button:GetAttribute('Price')
				
				if Price then
					if player.leaderstats.Cash.Value < Price then
						warn('You cannot afford this item!')
						return
					end
					
					player.leaderstats.Cash.Value -= Price
					Bought = true
				end
				
				for _, item in TempItems:GetChildren() do
					if item:GetAttribute('Id') ~= itemToUnlockId then continue end
					
					local itemClone = item:Clone()
					
					local itemCFrame
					
					if itemClone:IsA('Model') then
						itemCFrame = itemClone:GetPivot() --Model
					elseif itemClone:IsA('BasePart') then
						itemCFrame = itemClone.CFrame --BasePlate (Part, Union, Mesh, etc.)
					end
					
					local relativeItemCFrame = TempPlot.CFrame:ToObjectSpace(itemCFrame)
					local worldCFrameOfNewPlot = plot.CFrame:ToWorldSpace(relativeItemCFrame)
					if itemClone:IsA('Model') then
						itemClone:PivotTo(worldCFrameOfNewPlot)
					elseif itemClone:IsA('BasePart') then
						itemClone.CFrame = worldCFrameOfNewPlot
					end
					
					-- Updates the player's income to Revenue attribute in button
					local Income = player.leaderstats.Income
					local Revenue = Button:GetAttribute('Revenue')
					Income.Value += Revenue
					
					itemClone.Parent = itemsFolder
					Button:Destroy()
					print('Button has been pressed by '..player.Name..' and their income has been increased to '..Income.Value..'!')
					Running = false
				end
			end)