Bug with custom ProximityPrompt

hi, i want to get some tips on writing scripts, my code works, but it looks sloppy. There is one problem related to the fact that holding down a key can cause a crash if the player moves the mouse or moves, but this rarely happens.

i have my custom ProximityPrompt, if you can still call it that, it works, but as I said, there are some minor problems.

-- Module

-- fast navigation
local nav = {
	replic = game:GetService("ReplicatedStorage"); player = game:GetService("Players").LocalPlayer; prompt = script.Prompt;
	input = game:GetService("UserInputService"); mobile = game:GetService("UserInputService").TouchEnabled; runservice = game:GetService("RunService");
	--..
}

local Proximity = {}

-- create proximity
function Proximity.new(...)
	local a = {...}
	--print(a)
	
	local function action(...)
		print("action")
	end
	
	local function proximity() -- create screengui
		if not nav.player:WaitForChild("PlayerGui", 2):FindFirstChild("ProximityPrompts") then
			local proximity = Instance.new("ScreenGui");
			proximity.Name = "ProximityPrompts";
			proximity.Parent = nav.player:WaitForChild("PlayerGui", 2);

			proximity.SafeAreaCompatibility = Enum.SafeAreaCompatibility.FullscreenExtension;
			proximity.ScreenInsets = Enum.ScreenInsets.DeviceSafeInsets;
			proximity.IgnoreGuiInset = true;
		end
	end
	
	local function prompt() -- create prompt
		proximity()
		
		local billprompt = nav.prompt:Clone()
		billprompt.Adornee = a[2]
		billprompt.Parent = nav.player:WaitForChild("PlayerGui", 2):FindFirstChild("ProximityPrompts")
		billprompt.Frame.TextFrame.ActionText.Text = a[6]; billprompt.Frame.TextFrame.ObjectText.Text = a[5] billprompt.Frame.InputFrame.ButtonText.Text = a[3]
		
		local textbutton = billprompt.TextButton; local promt = billprompt.Frame; local down = false; local timer = 0
		
		local function color(...)
			local a = {...}
			
			if a[1] == true then
				promt.UIScale.Scale = .8;
				promt.InputFrame.BackgroundColor3 = Color3.fromRGB(55, 55, 59);
				promt.TextFrame.BackgroundColor3 = Color3.fromRGB(55, 55, 59);
			elseif a[1] == false then
				promt.UIScale.Scale = 1;
				promt.InputFrame.BackgroundColor3 = Color3.fromRGB(14, 14, 15);
				promt.TextFrame.BackgroundColor3 = Color3.fromRGB(14, 14, 15);
			end
		end
		
		local function press()
			local right = billprompt.Frame.InputFrame.CircularProgressBar["Frame-1"].ImageLabel
			local left = billprompt.Frame.InputFrame.CircularProgressBar["Frame-2"].ImageLabel
			local coldown = false
			
			local rotation = 0
			
			--if coldown == true then return end
			
			if a[4] and coldown == false then
				if a[4] ~= 0 then
					color(true)
					down = true
					
					local length = a[4] * 30; local CL = 0
					local cir = task.spawn(function()
						repeat wait()
							CL += 1; billprompt.Frame.InputFrame.CircularProgressBar.Progress.Value = CL/length * 100
							local percent = math.clamp(billprompt.Frame.InputFrame.CircularProgressBar.Progress.Value * 3.6, 0, 360)
							if percent <= 180 then
								right.UIGradient.Rotation = percent
								left.UIGradient.Rotation = 180
							else
								right.UIGradient.Rotation = 180
								left.UIGradient.Rotation =  percent
							end
						until CL/length * 100 >= 100 or down == false
						color(false)
					end)

					local tim = task.spawn(function()
						repeat wait(.1)
							timer += .1
							if timer >= a[4] then
								action()
							end
						until timer >= a[4] or down == false
						color(false)
						right.UIGradient.Rotation = 0; left.UIGradient.Rotation = 180
						down = false; timer = 0
					end)
				elseif a[4] == 0 then
					color(true); task.wait(.1); color(false)
					down = false; timer = 0
					action()
				end
			end
		end
		
		if not nav.mobile then
			nav.input.InputBegan:Connect(function(put) -- keybind
				if put.KeyCode == Enum.KeyCode[a[3]] then
					press()
				end
			end)
			nav.input.InputEnded:Connect(function() -- keybind
				down = false; timer = 0
			end)
			textbutton.MouseButton1Down:Connect(function() -- pc
				press()
			end)
			textbutton.MouseButton1Up:Connect(function() -- pc
				down = false; timer = 0
			end)
		elseif nav.mobile then
			textbutton.TouchTap:Connect(function() -- mobile
				press(); down = false; timer = 0
			end)
			textbutton.TouchLongPress:Connect(function() -- mobile
				press()
			end)
		end
	end
	
	prompt()
end

return Proximity

-- LocalScript

-- fast navigation
local nav = {
	replic = game:GetService("ReplicatedStorage"); proxy = require("@game/ReplicatedStorage/Modules/Proximity");
	player = game:GetService("Players").LocalPlayer;
	--..
}

-- managing Proximity being created
nav.proxy.new(
	nav.player, --..player
	workspace:WaitForChild("Part", 4), --..part
	"E", --..keybind
	0, --..cooldown press (0 = not cooldown for base click)
	"Click", --..type
	"John Doe" --..text
)

nav.proxy.new(
	nav.player,
	workspace:WaitForChild("Part-1", 4),
	"R",
	3,
	"Hold",
	"Roblox"
)

There were a few things that caught my attention that could possibly cause the lag and crashes.

First, using repeat wait() for the hold duration/progress bar is not the best idea. wait() is throttled in heavy conditions and is not very precise, so it could make your animation less smooth. I’d recommend using either TweenService or RunService.RenderStepped here.

Secondly, I’ve seen you are making global InputBegan connections inside Proximity.new, but you are never disconnecting them. In case multiple prompts are made throughout the gameplay, you will end up having the same amount of connections each time, and it can result in duplicate handling and extra memory usage.

Another thing is that TouchEnabled is being used for the mobile check. However, touch-enabled laptops also have TouchEnabled = true. So, players who own laptops that have the touch screen will still get the mobile path even though they are using a regular keyboard. It would be better to use UserInputService.LastInputTypeChanged (or the last input type).

One more thing I would like to say is that you can replace your custom prompt setup by hooking into ProximityPromptService.PromptShown instead.

1 Like

This code honestly feels intentionally confusing.

I refuse to believe it ended up this way by accident.

Not only do I not see an optimization benefit here, I also find it significantly harder to follow than it needs to be.

Instead, I’d focus on removing unnecessary indirection.

If this entire module only returns a single function, why not simply do:

return function():()

end

Next:

Why are you waiting for PlayerGui?

I’d cache the instance path instead.

The logic also seems to contradict itself.

You break out of the wait loop after 2 seconds, which means PlayerGui can still be nil, yet you immediately do PlayerGui:FindFirstChild(...).

And even when the condition succeeds, you still call WaitForChild on PlayerGui.

That’s exactly why clarity matters.

Because:

Clarity :handshake: Optimization :handshake: Organization

They reinforce each other rather than compete.

TL;DR: Stop being afraid of your code.

Don’t run from it.

Own it.

1 Like

thanks guys for recommendations and mistakes in my code, i took into account what i could understand and rewrote the module completely.

as for why i use the repeat wait() method, i do not know, it just works and does not give errors, to be honest, i tried to use RunService, but i had problems with it when measuring part of the circle on the panel on the button, so i decided to leave repeat.

but still, thank you for your opinion, i will take your recommendations into account in future projects. (bugs with buttons are gone) >_o

-- @B1tyDev // 06.08.2026
-- ProximityPrompt Module

local navigation = { -- easy navigation services
	replicatedstorage = game:GetService("ReplicatedStorage");
	userinputservice = game:GetService("UserInputService");
	players = game:GetService("Players");
	--runservice = game:GetService("RunService");
	--...
	prompt = script.Prompt;
}

local ProximityPrompt = {}

return function(...):() -- function
local a = {...}
local playerGui = a[1]:FindFirstChild("PlayerGui")
--print(a)

local function action() -- activation
	-- a[7] event name
	navigation.replicatedstorage.Remotes:FindFirstChild("RemoteEvent"):FireServer(a[7])
	print("activation")
end

local function proximityPrompts() -- create screengui
	if not playerGui:FindFirstChild("ProximityPrompts") then
		local proximity = Instance.new("ScreenGui")
		proximity.Name = "ProximityPrompts"
		proximity.Parent = playerGui
		proximity.SafeAreaCompatibility = Enum.SafeAreaCompatibility.FullscreenExtension
		proximity.ScreenInsets = Enum.ScreenInsets.DeviceSafeInsets
		proximity.IgnoreGuiInset = true
	end
end

local function proximity() -- create prompt
	proximityPrompts() -- initialize screengui
	local prompt = navigation.prompt:Clone()
	prompt.Adornee = a[2]
	prompt.Parent = playerGui:FindFirstChild("ProximityPrompts")
	prompt.Frame.TextFrame.ActionText.Text = a[6]
	prompt.Frame.TextFrame.ObjectText.Text = a[5]
	prompt.Frame.InputFrame.ButtonText.Text = a[3]
	
	local textbutton = prompt.TextButton
	local promt = prompt.Frame
	local key = false
	local timer = 0
	
	local function changeColor(action) -- change color for prompt frame
		if action == true then
			promt.UIScale.Scale = .8
			promt.InputFrame.BackgroundColor3 = Color3.fromRGB(55, 55, 59)
			promt.TextFrame.BackgroundColor3 = Color3.fromRGB(55, 55, 59)
		elseif action == false then
			promt.UIScale.Scale = 1
			promt.InputFrame.BackgroundColor3 = Color3.fromRGB(14, 14, 15)
			promt.TextFrame.BackgroundColor3 = Color3.fromRGB(14, 14, 15)
		end
	end
	
	local function press() -- key/touch/click
		if a[4] ~= 0 then
			key = true
			changeColor(true)
			local circular = task.spawn(function() -- visual circular progress bar
				local legth = a[4]*30
				local cl = 0
				repeat wait()
					cl += 1
					promt.InputFrame.CircularProgressBar.Progress.Value = cl/legth * 100
					local percent = math.clamp(promt.InputFrame.CircularProgressBar.Progress.Value * 3.6, 0, 360)
					if percent <= 180 then
						promt.InputFrame.CircularProgressBar["Frame-1"].ImageLabel.UIGradient.Rotation = percent
						promt.InputFrame.CircularProgressBar["Frame-2"].ImageLabel.UIGradient.Rotation = 180
					else
						promt.InputFrame.CircularProgressBar["Frame-1"].ImageLabel.UIGradient.Rotation = 180
						promt.InputFrame.CircularProgressBar["Frame-2"].ImageLabel.UIGradient.Rotation =  percent
					end
				until cl/legth * 100 >= 100 or key == false
				changeColor(false)
				promt.InputFrame.CircularProgressBar.Progress.Value = 0
				promt.InputFrame.CircularProgressBar["Frame-1"].ImageLabel.UIGradient.Rotation = 0
				promt.InputFrame.CircularProgressBar["Frame-2"].ImageLabel.UIGradient.Rotation = 180
			end)
			
			local timer = task.spawn(function() -- function check time
				repeat wait(.1)
					timer += .1
					if timer >= a[4] then
						action()
					end
				until timer >= a[4] or key == false
				task.cancel(circular)
				changeColor(false)
				promt.InputFrame.CircularProgressBar.Progress.Value = 0
				promt.InputFrame.CircularProgressBar["Frame-1"].ImageLabel.UIGradient.Rotation = 0
				promt.InputFrame.CircularProgressBar["Frame-2"].ImageLabel.UIGradient.Rotation = 180
				key = false
				timer = 0
			end)
		elseif a[4] == 0 then -- no cooldown, base click
			action()
			changeColor(true)
			task.wait(.1)
			changeColor(false)
			key = false
		end
	end
	
	navigation.userinputservice.LastInputTypeChanged:Connect(function(inputType) -- check input type
		if inputType == Enum.UserInputType.Keyboard then -- keybind
			local enter = navigation.userinputservice.InputBegan:Connect(function(inputObject) -- keybind start
				if inputObject.KeyCode == Enum.KeyCode[a[3]] then
					if not key then
						key = true
						press()
					else
						return
					end
				end
			end)
			local leave = navigation.userinputservice.InputEnded:Connect(function(inputObject) -- keybind end
				if inputObject.KeyCode == Enum.KeyCode[a[3]] then
					if key then
						key = false
						timer = 0
					else
						return
					end
				end
			end)
		elseif inputType == Enum.UserInputType.MouseButton1 then -- mouse click
			local enter = navigation.userinputservice.InputBegan:Connect(function(inputObject)
				textbutton.MouseButton1Click:Connect(function()
					if not key then
						key = true
						press()
					else
						return
					end
				end)
			end)
		elseif inputType == Enum.UserInputType.Touch then -- touch click
			local enter = navigation.userinputservice.InputBegan:Connect(function(inputObject)
				if inputObject.UserInputType == Enum.UserInputType.Touch then
					textbutton.InputBegan:Connect(function()
						if not key then
							key = true
							press()
						else
							return
						end
					end)
				end
			end)
		end
	end)
end

proximity() -- run function proximity
end
-- Client

local navigation = {
	replicatedstorage = game:GetService("ReplicatedStorage");
	players = game:GetService("Players");
	proximity = require("@game/ReplicatedStorage/Modules/ProximityPrompt"); -- require the module
	--...
}

-- create new proximity prompt
navigation.proximity(
	navigation.players.LocalPlayer, -- player
	workspace:WaitForChild("Part"), -- insert part
	"E", -- keybind
	0, -- cooldown
	"Test", -- type
	"Prototype", -- text
	"test" -- event name
)

navigation.proximity(
	navigation.players.LocalPlayer, -- player
	workspace:WaitForChild("Part-1"), -- insert part
	"R", -- keybind
	2, -- cooldown
	"Test", -- type
	"Prototype", -- text
	"test2" -- event name
)

Preformatted text