ProximityPrompt not being destroyed

I’m making a system where you activate a prompt on a part, and it clones to the player and hangs around them. But for some reason, it seems kind of laggy and the part still has the proximity prompt even though it doesnt?


Screenshot 2023-02-20 at 17.02.18

https://gyazo.com/e374ec1b27cf79fb5f81fcd2b7954a02
(Even if the video is lagging you can see the part doesn’t keep next to me like a weld would)

local RunService = game:GetService("RunService")

local maxNoParts = 4

local function PartsFound(t): tuple
	local PartsFound = {}
	for i, v in ipairs(t:GetChildren()) do
		for i=1, maxNoParts do
			if v.Name == tostring(i) then
				PartsFound[v] = v
			end
		end
	end
	return PartsFound
end

local function UnderMaxParts(t): boolean
	local PartsFound = #PartsFound(t)
	
	if PartsFound <= maxNoParts then return true end
end

local function NamePart(t): string
	for i=1, maxNoParts do
		if t:FindFirstChild(tostring(i)) then
			return tostring(i+1)
		end
	end
	return "1"
end

for i, part in ipairs(workspace.Folder:GetChildren()) do
	local Prompt: ProximityPrompt = part.ProximityPrompt
	Prompt.Triggered:Connect(function(player)
		task.defer(function()
			local Character = player.Character
			local hrp = Character.HumanoidRootPart
			if UnderMaxParts(Character) then
				local newPart = part:Clone()
				newPart.Anchored = true
				newPart.CanCollide = false
				newPart.Name = NamePart(Character)
				newPart.ProximityPrompt:Destroy()
				print(newPart:GetChildren())
				newPart.Parent = Character

				RunService.Heartbeat:Connect(function()
					local NumberOfParts = #PartsFound(Character)
					print(NumberOfParts)
					local Angle = NumberOfParts*2*math.pi/maxNoParts
					local Radius = 5
		
					local PositionOnCircle = Vector3.new(math.sin(Angle), 0, math.cos(Angle))*Radius

					local position = (hrp.CFrame * CFrame.new(PositionOnCircle)).Position -- .p gets the position part of the CFrame as a Vector3
					part.CFrame = CFrame.new(position)
				end)
			end
		end)
	end)
end

Try removing this. Events already spawn in a separate thread.

1 Like

I’m not using task.defer because I want a seperate thread like coroutine, just because.
I wouldn’t think it would make a difference

I removed it and it didn’t have an effect

1 Like

Keeping less threads is better for performance.


Now, I realize that it’s a server script. This should be handled on the client instead. It seems really easy to switch over as well.


Now, after further inspecting your code, you’re rotating the original part and not the new one:

The reason why it’s lagging is because you repeatedly print 0 in the number of parts, which is a bad practice.

Not only that, but there’s another issue:


In addition, the max parts function does not work at all. I ended up combining the functions:

local function NamePart(t): string
	local maxNum
	for i=1, maxNoParts do
		if t:FindFirstChild(tostring(i)) then
			if i+1 > maxNoParts then
				return
			end
			maxNum = tostring(i+1)
		end
	end
	return maxNum or "1"
end

If the function does not return anything, the part will not be created.

In addition, the partsFound function does not work. I fixed this by implementing attributes:

local function PartsFound(t): number
	local PartsFound = 0
	for i, v in t:GetChildren() do
		if v:GetAttribute("isPart") then
			PartsFound += 1
		end
	end
	return PartsFound
end

Now, it returns a number.

Oh, the parts also go to the same spot because it’s calculated by the total of the parts and not the number of the parts. I did tonumber(name) instead:

local nameNumber = tonumber(name)
		RunService.Heartbeat:Connect(function()
			--local NumberOfParts = PartsFound(Character)
			--print(NumberOfParts)
			--local Angle = NumberOfParts*2*math.pi/maxNoParts
			local Angle = nameNumber*2*math.pi/maxNoParts
			local Radius = 5

			local PositionOnCircle = Vector3.new(math.sin(Angle), 0, math.cos(Angle))*Radius

			local position = (hrp.CFrame * CFrame.new(PositionOnCircle)).Position -- .p gets the position part of the CFrame as a Vector3
			newPart.CFrame = CFrame.new(position)
		end)

That means we no longer need the PartsFound function.


I also made it compatible with redetecting the character if, for example, the player resets their character:

if not Character or not Character.Parent then
				Character = player.Character
				if Character then
					hrp = Character:FindFirstChild("HumanoidRootPart")
					if not hrp then return end
				else
					return
				end
			end

That’s not all. Since it is now a wonderful local script, we need to take into account the performance benefits of single-thread operations. So, we will add these parts to a table and update them all at once every frame, instead of making multiple connections to heartbeat.

So, what I did, was I have a table called parts, and I insert the part when it is “interacted with”. Then, each frame, I iterate over all the parts in parts and update them.

RunService.Heartbeat:Connect(function(deltaTime)
	if not Character or not Character.Parent then
		Character = player.Character
		if Character then
			hrp = Character:FindFirstChild("HumanoidRootPart")
			if not hrp then return end
		else
			return
		end
	end
	for num, part in parts do
		local Angle = num*2*math.pi/maxNoParts
		local Radius = 5

		local PositionOnCircle = Vector3.new(math.sin(Angle), 0, math.cos(Angle))*Radius

		local position = (hrp.CFrame * CFrame.new(PositionOnCircle)).Position -- .p gets the position part of the CFrame as a Vector3
		part.CFrame = CFrame.new(position)
	end
end)

This will make nameNumber, which was defined earlier, obsolete. Now, we can just use the number provided by the for loop!

But what if the player wants to replace the part? They can’t do that yet. So, I implemented a method for them to remove the part.

First, we need to update the NamePart function to something like this:

local function NamePart(t): string
	return tostring(#parts+1)
end

This will take the length of the table plus one and turn it into a string. Now, we need to remove the section of the script where it returns when there isn’t a name for the part:

if not name then return end --new limit to max parts

And replace it with:

if #parts >= maxNoParts then return end

But, if we want the player to be able to replace the oldest part in the list, we would do this:

if #parts >= maxNoParts then
	table.remove(parts, 1)
end

This will most likely remove the last part in the table. I tested this out with two parts of different colors, and it worked. Well, it worked. The problem is that it doesn’t delete the oldest part. We can fix that though:

if #parts >= maxNoParts then
	local oldestPart = parts[1]
	if not oldestPart then
		warn('unexpected error')
		return
	end
	oldestPart:Destroy()
	table.remove(parts, 1)
end

Is it working now? Of course it is! Are we done yet? No!


Now, we need to solve something which probably was not thought of before:


Now, this is one of the easiest fixes.

local loadedParts = {}

function loadPart(part)
	if not part:IsA("BasePart") then return end
	if not loadedParts[part] then
		newPart(part)
	end
end



for i, part in partsFolder:GetChildren() do
	loadPart(part)
end

partsFolder.ChildAdded:Connect(loadPart)

partsFolder is assigned at the top of the script

Now we’re done? Nope. Replication.


Replicating this sort of stuff is not easy, but it is possible. Well, it’s quite harder than what you’d imagine. How should we replicate this? Do we display the updates on the server? Of course not!

We’ll tell the server that the client obtained a new part or removed the part. We need to implement server validation as well.

First, we need a server script preferably in ServerScriptService. Then, a remote event. The event in my case will be located in ReplicatedStorage.ReplicationEvents.PartUpdateEvent. It is defined in the LocalScript as:

local event = game:GetService("ReplicatedStorage"):WaitForChild("ReplicationEvents"):WaitForChild('PartUpdateEvent')

You can name it whatever you want though.

Now, we’re going to use the same event for both sending and receiving data. It might sound like a handful, but it’s quite straightforward.

First, in the local script, we will move that for loop described earlier to a function, with hrp and customParts as the parameter:

function updateParts(hrp, customParts)
	for num, part in customParts or parts do
		local Angle = num*2*math.pi/maxNoParts
		local Radius = 5

		local PositionOnCircle = Vector3.new(math.sin(Angle), 0, math.cos(Angle))*Radius

		local position = (hrp.CFrame * CFrame.new(PositionOnCircle)).Position
		part.CFrame = CFrame.new(position)
	end
end

This will allow us to update parts for a specific root part, and with a custom parts table. Speaking of which, more tables are needed for this to function:

local otherPlayerParts = {}

Now, before I forget what I’m doing, let’s move on to the server before continuing on the local script.


On the server, we must define the remote event as well:

local event = game:GetService("ReplicatedStorage").ReplicationEvents.PartUpdateEvent

However, we do not use Instance.WaitForChild because it is only meant to be used on the client. The dot operator in this case is faster.

Now, we can hook up a function when the client sends data:

event.OnServerEvent:Connect(function(playerFrom, part)
	
end)

What I’m planning to do here is make part the part that the player is attempting to add to their character. But first, let’s define a few more variables:

local maxNoParts = 4

We define this in the server script to validate that the player does not exceed this number of parts.

Also, remember that now-old “PartsFound” function? Yeah, we can’t use that on the server because the part does not exist on the server. We will instead store a table of all the parts each player has, indexed by the player instance.

Now, we have this:

local partsTable = {}

event.OnServerEvent:Connect(function(playerFrom, part)
	local character = playerFrom.Character
	if not character then return end
	local existingParts = partsTable[playerFrom] or 0
end)

Now, we need to send data back to every other player:

event.OnServerEvent:Connect(function(playerFrom, part)
	local character = playerFrom.Character
	if not character then return end
	local hrp = character:FindFirstChild("HumanoidRootPart")
	if not hrp then return end
	local existingParts = partsTable[playerFrom] or 0
	for _, player in players:GetPlayers() do
		if player == playerFrom then continue end
		event:FireClient(playerFrom, character, part)
	end
	partsTable[playerFrom] = math.clamp(partsTable[playerFrom] + 1, 0, maxNoParts)
end)

(I also added hrp)
Additionally, I send existingParts == maxNoParts to indicate if the player is “replacing” a part. It turns out we don’t need that, so I removed it. The event will also skip the playerFrom to avoid self-replication (for lack of a better word).

Now, we’re going back to the client side of things.


Now, we’ll handle replication to the server first, since that’s more straightforward.

Simply adding this line:

event:FireServer(part)

after this one:

table.insert(parts, newPart)

will allow replication to take effect.

So, what it currently does, is sends a part instance to the server (not the cloned one) and then the server sends it back to every other client.

Now, it’s receiving time:

event.OnClientEvent:Connect(function(playerFrom, character, part, hrp)
	
end)

And, before we move further, we’ll make the part creation function generic:

function createNewPart(part, character, customPartsTable)
	character = character or Character
	local parts = customPartsTable or parts
	if #parts >= maxNoParts then
		local oldestPart = parts[1]
		if not oldestPart then
			warn('unexpected error')
			return
		end
		oldestPart:Destroy()
		table.remove(parts, 1)
	end
	local name = NamePart(character)
	local newPart = part:Clone()
	newPart.Anchored = true
	newPart.CanCollide = false
	newPart.Name = name
	newPart:SetAttribute('isPart', true)
	newPart.ProximityPrompt:Destroy()
	--print(newPart:GetChildren())
	newPart.Parent = character
	return newPart
end

Now, we modify the “newPart” function:

function newPart(part)
	local Prompt: ProximityPrompt = part:FindFirstChild("ProximityPrompt")
	if not Prompt then return end
	Prompt.Triggered:Connect(function()
		local newPart = createNewPart(part)
		table.insert(parts, newPart)
		event:FireServer(part)
	end)
end

What the “createNewPart” function does is it will create a part regardless of if the proximity prompt was activated or not, and it will also parse a custom list of parts for replacing one.

What is this custom list of parts you ask? Well, it’s the “otherPlayerParts” table we described a half hour ago!

So, we can add to it in that receiving function. But first, we must actually create the part for the other player:

event.OnClientEvent:Connect(function(playerFrom, character, part)
	local playerParts = otherPlayerParts[playerFrom] or {}
	local newPart = createNewPart(part, character, playerParts)
end)

Then, insert it to the playerParts table in which we redefine otherPlayerParts[playerFrom] to:

event.OnClientEvent:Connect(function(playerFrom, character, part)
	local playerParts = otherPlayerParts[playerFrom] or {}
	local newPart = createNewPart(part, character, playerParts)
	table.insert(playerParts, newPart)
	otherPlayerParts[playerFrom] = playerParts
end)

Okay, this is good, but we aren’t really replicating it just yet. We need to update it each frame, just like how we update the local players’ parts. A simple loop can fix that. Also, did I say we needed to replicate hrp’s variable? We do not! Another table can fix that. Now, our function is this:

local humanoidRootPartCache = {}

function updateOtherPlayerParts()
	for player, customParts in otherPlayerParts do
		if not player.Character then continue end
		local hrp = humanoidRootPartCache[player.Character]
		if not hrp then
			hrp = player.Character:FindFirstChild("HumanoidRootPart")
			if not hrp then continue end
			humanoidRootPartCache[player.Character] = hrp
		end
		updateParts(hrp, customParts)
	end
end

And we can call it by itself in the heartbeat update function.


Now, there’s a problem with our current system. Can you spot it?

I couldn’t for two minutes, don’t feel bad.

Okay, there’s two problems. The system doesn’t respond to the proximity prompt being activated. Why? Because we forgot to use WaitForChild:

local Prompt: ProximityPrompt = part:WaitForChild("ProximityPrompt", 1)

Then, there’s a problem on the server, where it attempts to add to a value that does not exist here:

partsTable[playerFrom] = math.clamp(partsTable[playerFrom] + 1, 0, maxNoParts)

The corrected code is:

partsTable[playerFrom] = math.clamp(existingParts + 1, 0, maxNoParts)

Also, to prevent further issues, we will replace this:

newPart.ProximityPrompt:Destroy()

with this:

local oldPrompt = newPart:FindFirstChild("ProximityPrompt")
if oldPrompt then oldPrompt:Destroy() end

Now, since I made this code first try, I expected there to be at least one more bug after this. Yep:
image
This is happening because the server is somehow sending the character and not the playerFrom as the first parameter.

Ah, I’m sending the event to the playerFrom:

event:FireClient(playerFrom, character, part)

it should be:

event:FireClient(player, playerFrom, character, part)

But now, finally, it works!


There’s just one more thing we need to solve. That is, if a player joints after one player already collected some parts, they will not see it. I’m going to provide the updated server script code below:

local event = game:GetService("ReplicatedStorage").ReplicationEvents.PartUpdateEvent
local players = game:GetService("Players")

local maxNoParts = 4

local partsTable = {}

function addPart(playerFrom, part, specificPlayer)
	local character = playerFrom.Character
	if not character then return end
	local hrp = character:FindFirstChild("HumanoidRootPart")
	if not hrp then return end
	if specificPlayer then
		event:FireClient(specificPlayer, playerFrom, character, part)
	else
		local existingParts = partsTable[playerFrom] or {}
		for _, player in players:GetPlayers() do
			if player == playerFrom then continue end
			event:FireClient(player, playerFrom, character, part)
		end
		table.insert(existingParts, part)
		partsTable[playerFrom] = existingParts
	end
end

event.OnServerEvent:Connect(addPart)

players.PlayerAdded:Connect(function(player)
	task.wait(3) --wait for player to load in
	for playerFrom, parts in partsTable do
		for _, part in parts do
			addPart(playerFrom, part, player)
		end
	end
end)

Also, I missed a line in the update parts section:

part.Parent = hrp.Parent

This will allow parts to continue to focus on the player, even if they reset.

Now, we’re finally done with the system.



TL;DR
I fixed your code. It is provided in this place file:
partOrbit.rbxl (42.3 KB)
Also, what shall you call this system you made?

2 Likes

Holy I fixed it by myself but thanks for all this, Defo marking as solution

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