Loop not ending

I’m trying to make a mechanic that allows you to repair generators in my game, when you press mousebutton1 you start fixing it and when you release it, you stop fixing it.

I have it set up on a local script to trigger a proximityprompt when you press mousebutton1

The only problem I have with this is when the player repairs the generator it fires a loop (which is inside of a coroutine) that increases the progress of the generator every .5 seconds and for some reason if you spam it, progress is faster and faster.

The Main Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local GeneratorManager = require(script:WaitForChild("GeneratorManager"))
local AnimationSetup = require(script:WaitForChild("AnimationSetup"))
local animations = require(script:WaitForChild("GeneratorManager"):WaitForChild("Animations"))

local PositionReplication = ReplicatedStorage:WaitForChild("RemoteEvents"):WaitForChild("CharacterReplication"):WaitForChild("PositionReplication")
local PlayerMouseInputEndedEvent = ReplicatedStorage:WaitForChild("RemoteEvents"):WaitForChild("PlayerReplication"):WaitForChild("PlayerMouseInputEnded")

local animationsToPreload = {
	animations.generatorRepair
}

AnimationSetup.PreloadAnimations(animationsToPreload)

local function GetClosestSide(plr, parts)
	local closestPart
	local playerPosition

	PositionReplication:FireClient(plr)

	local connection
	connection = PositionReplication.OnServerEvent:Connect(function(sender, pos)
		if sender == plr then
			playerPosition = pos
			connection:Disconnect()
		end
	end)

	while not playerPosition do
		task.wait()
	end

	for _, part in pairs(parts) do
		local distance = (part.Position - playerPosition).Magnitude
		if not closestPart or distance < (closestPart.Position - playerPosition).Magnitude then
			closestPart = part
		end
	end

	return closestPart
end

for _, prompt in pairs(game.Workspace:GetDescendants()) do
	if prompt:IsA("ProximityPrompt") and prompt:GetAttribute("Generator") == true then
		local GeneratorConfig = GeneratorManager.ConfigureGenerator(prompt)

		prompt.Triggered:Connect(function(plr)
			if GeneratorConfig.generatorCompleted then return end

			local char = plr.Character or plr.CharacterAdded:Wait()
			local humRoot = char:WaitForChild("HumanoidRootPart")

			local closestSide = GetClosestSide(plr, GeneratorConfig.OpenPos)
			GeneratorConfig:FixGeneratorStart(plr, closestSide)
		end)

		PlayerMouseInputEndedEvent.OnServerEvent:Connect(function(plr)
			GeneratorConfig:FixGeneratorEnd(plr)
		end)
	end
end

The Module Script

local GeneratorManager = {}
GeneratorManager.__index = GeneratorManager

local animations = require(script.Animations)
local localManagement = require(game.ServerStorage:WaitForChild("Optimization"):WaitForChild("LocalManagement"))
local characterManager = require(game.ServerStorage:WaitForChild("CharacterManager"))

local ProgressBarEvent = game.ReplicatedStorage:WaitForChild("RemoteEvents"):WaitForChild("GuiEvents"):WaitForChild("SignalProgressBar")

local lockinfo = {0.3, Enum.EasingStyle.Sine}

function GeneratorManager.ConfigureGenerator(prompt)
	local promptConfig = {}

	promptConfig.progress = 0
	promptConfig.generatorCompleted = false
	promptConfig.playersFixing = {}
	promptConfig.activeCoroutines = {}
	promptConfig.debounceTable = {}
	promptConfig.prompt = prompt
	promptConfig.generator = prompt.Parent.Parent
	promptConfig.OpenPos = {
		prompt.Parent.Parent.FixPos1,
		prompt.Parent.Parent.FixPos2,
		prompt.Parent.Parent.FixPos3,
		prompt.Parent.Parent.FixPos4,
	}

	setmetatable(promptConfig, GeneratorManager)
	return promptConfig
end

function GeneratorManager:FixGeneratorStart(plr, closestSide)
	
	if self.debounceTable[plr] and tick() - self.debounceTable[plr] < 0.3 then return end
	self.debounceTable[plr] = tick()

	if self.activeCoroutines[plr] then return end

	table.insert(self.playersFixing, plr)
	ProgressBarEvent:FireClient(plr, self.progress / 100, true)

	local char = plr.Character or plr.CharacterAdded:Wait()
	local plrHumRoot = char:WaitForChild("HumanoidRootPart")
	local hum = char:WaitForChild("Humanoid")
	local plrAnimator = hum:WaitForChild("Animator")

	plrHumRoot.Anchored = true
	characterManager:SetPlayerTurn(plr, false)
	localManagement:PlayAnimation(plr, animations.generatorRepair.AnimationId, plrAnimator)
	localManagement:SetModelCollision(plr, self.generator, true)
	localManagement.Tween(plrHumRoot, lockinfo, {CFrame = closestSide.CFrame})

	local cor = coroutine.create(function()
		local LastTick = tick()
		while true do
			task.wait()
			if not table.find(self.playersFixing, plr) then break end
			if tick() - LastTick > 0.5 then
				LastTick = tick()
				self.progress += 1
				ProgressBarEvent:FireClient(plr, self.progress / 100, true)
			end
		end
	end)

	self.activeCoroutines[plr] = cor
	coroutine.resume(cor)
end

function GeneratorManager:FixGeneratorEnd(plr)
	if not table.find(self.playersFixing, plr) then return end

	local index = table.find(self.playersFixing, plr)
	table.remove(self.playersFixing, index)

	ProgressBarEvent:FireClient(plr, self.progress / 100, false)

	local char = plr.Character or plr.CharacterAdded:Wait()
	local plrHumRoot = char:WaitForChild("HumanoidRootPart")
	local hum = char:WaitForChild("Humanoid")
	local plrAnimator = hum:WaitForChild("Animator")

	plrHumRoot.Anchored = false
	characterManager:SetPlayerTurn(plr, true)
	localManagement:StopAnimation(plr, animations.generatorRepair.AnimationId, plrAnimator, nil, nil, true)

	local cor = self.activeCoroutines[plr]
	if cor then
		coroutine.close(cor)
		self.activeCoroutines[plr] = nil
	end

	self.debounceTable[plr] = nil
end

return GeneratorManager

OOP is pretty bad idea to use for such systems.
Use ECS instead;
tick better be replaces with os.clock()

	while not playerPosition do
		task.wait()
	end

Is honestly a bad part of code.

You better make it using thread management:

coroutine.yield() and awake this thread from outside using coroutine.resume()

Please don’t use pairs/ipairs in dawg 2025 :wilted_flower: cause inext and next absolutelly sucks at perfomance.
Generic iteration is better.

2 Likes

I was looking up ECS and reading up on it but I don’t really understand what’d id use it for and how I’d use it in my case. Sorry for the late reply but I would like to figure out ECS.

Also I have a pallet system that used OOP, where you can knock over and vault pallets. Should i also be using ECS for that instead?

Short: have a system instead of individual objects.
Instead of methods have functions.

1 Like

Have you ensured (using debug prints or exposing the value somehow) that it’s actually increasing faster by spamming?

I hate to ask such a question, but if your code is running single-threaded (not part of an Actor), then it seems to be quite bulletproof. I really can’t find any way that the coroutine would keep running. You should be putting prints everywhere, if you haven’t already to try to feel it out.

2 Likes

I’ve been studying up on ECS and I’m gonna try to implement it, I’ll get back to you guys in a few days.

1 Like

Put a print statement under the GeneratorManager:FixGeneratorEnd(plr) function to check up until what point it reaches, and if cor even exists in the table

like this:

function GeneratorManager:FixGeneratorEnd(plr)
	print(1)
	if not table.find(self.playersFixing, plr) then return end
	print(2)

	local index = table.find(self.playersFixing, plr)
	print(index)
	table.remove(self.playersFixing, index)

	ProgressBarEvent:FireClient(plr, self.progress / 100, false)

	local char = plr.Character or plr.CharacterAdded:Wait()
	local plrHumRoot = char:WaitForChild("HumanoidRootPart")
	local hum = char:WaitForChild("Humanoid")
	local plrAnimator = hum:WaitForChild("Animator")

	plrHumRoot.Anchored = false
	characterManager:SetPlayerTurn(plr, true)
	localManagement:StopAnimation(plr, animations.generatorRepair.AnimationId, plrAnimator, nil, nil, true)

	local cor = self.activeCoroutines[plr]
	print(cor)
	if cor then
		coroutine.close(cor)
		self.activeCoroutines[plr] = nil
	end

	self.debounceTable[plr] = nil
end
1 Like