How to prevent functions containing wait() from overlapping?

Hi everyone! I already checked the DevForum but none of the answers were particularly helpful, mostly because it involved methods that were a bit too advanced for my skill level and struggled to implement them in my project.
The title explains my problem pretty well already; i have a function that is fired everytime a remote event is triggered, however the previous event ends up overlapping with the next one if the task.wait() isn’t over yet.
Here is the code:

Remote.OnServerEvent:Connect(function(plr)
	local char = plr.Character or plr.CharacterAdded:Wait()
	local HRP = char.HumanoidRootPart
	local runesFolder = char:FindFirstChild("Runes"):GetChildren()
	
	--Ignore this part, it's irrilevant
	if #runesFolder == 0 then
		print("Generate a rune first!")
		return
	elseif runesFolder[1]:GetAttribute("Type") == 1 then --Damage boost
		runesFolder[1]:Destroy()
		char:SetAttribute("activeRune",1)
	elseif runesFolder[1]:GetAttribute("Type") == 2 then --Regen energy
		runesFolder[1]:Destroy()
		char:SetAttribute("activeRune",2)
		
	elseif runesFolder[1]:GetAttribute("Type") == 3 then --Heal
		runesFolder[1]:Destroy()
		char:SetAttribute("activeRune",3)
	end

	--This is the part causing problems
	task.wait(10)
	char:SetAttribute("activeRune","None")
end)

Basically, if i fire the remote two or more times, the char:SetAttribute("activeRune","None") line is run when the time left from the previous thread(s) is over.
So my question is: how can I reset the wait time everytime the remote is fired?
Thanks in advance!!

What we can do is have a global variable to record the last time it was fired. To do this, get the time and store it in a local variable in your call. Then, store it globally. If, after 10 seconds, the global and local variable times match, you know a more recent call hasn’t been triggered.

There is a concept called debounce that basically makes you wait to call something until a certain amount of time has passed or some condition has been met. However, what you are asking for is not quite a debounce. If it is, that’s a different thing than what I’m doing above.

1 Like

it’s actually very simple, you create a debounce.

local debounce = false

remote.OnServerEvent:Connect(function()
    if not debounce then
       -- code goes here 

       debounce = true
    end
end)

This waits until the function finishes before allowing it to run again.

2 Likes

By global variable, you mean something using _G.? And the time, something that involves tick() right?

I did try with a debounce, however that almost acts as a cooldown and prevents me from firing the top part of the code (the one under the “Ignore this part, it’s irrilevant” comment) before the 10 seconds are over. I’d like to be able to fire the remote at any time AND reset the wait() at the same time.

Thank you so much both of you for trying to help! @Icee444 @batteryday

1 Like

You then make a debounce for only the yielding code

local debounce = false

remote.OnServerEvent:Connect(function()
    -- non yielding code goes here
    if not debounce then
       -- yielding code goes here 

       debounce = true
    end
end)
1 Like

_G is the global environment. You shouldn’t be using it in any scenario ever as far as Luau is concerned on Roblox. A global variable is just a value that isn’t localized, ie. you didn’t explicitly type local to define it. Again though, you don’t even need that (it also indexes at a slower speed than local variables).

batteryday is correct, all you really need is a variable at a higher scope, not a global variable.

1 Like

Ya, I wasn’t really thinking about using it anyway since I’m aware it’s considered a bad practice.

Tried, still the same result. If I fire the remote, wait 5 seconds, then fire it again, the wait() is still 5 seconds, not 10 aka they still overlap… @Icee444

local Remote = game:GetService("ReplicatedStorage"):FindFirstChild("Elemental")
local infuseBindable = script.InfuseShard
local db = false

Remote.OnServerEvent:Connect(function(plr)
	local char = plr.Character or plr.CharacterAdded:Wait()
	local HRP = char.HumanoidRootPart
	local runesFolder = char:FindFirstChild("Runes"):GetChildren()


	if #runesFolder == 0 then
		print("Generate a rune first!")
		return
	elseif runesFolder[1]:GetAttribute("Type") == 1 then --Damage boost
		runesFolder[1]:Destroy()
		char:SetAttribute("activeRune",1)
	elseif runesFolder[1]:GetAttribute("Type") == 2 then --Regen energy
		runesFolder[1]:Destroy()
		char:SetAttribute("activeRune",2)

	elseif runesFolder[1]:GetAttribute("Type") == 3 then --Heal
		runesFolder[1]:Destroy()
		char:SetAttribute("activeRune",3)
	end

	if not db then
		db = true
		task.wait(10) 
		char:SetAttribute("activeRune","None")	
		db = false
	end


end)

That is expected behavior. Why is that not what you want?

If you are saying you want the debounce code to run after 10 seconds + 10 seconds for everytime the event is fired, you can do that too by making a timer but it doesn’t really make any sense. Plus exploiters can spam remotes.

I think you are getting yourself confused.

Yeah, I think I explained myself already… :sweat_smile: English isn’t my first language, so perhaps it wasn’t worded properly.

It does make sense tho? The function I posted is basically a skill which triggers a buff; said buff is meant to last 10 seconds and its duration is refreshed if you re-use the skill before the buff expires. The problem is, the buff isn’t refreshed and keeps the duration of the previous time you used the skill. A timer, I suppose, is what I need. A debounce would simply act as a cooldown as I said, which isn’t the behavior i’m trying to apply.

1 Like

You made sense in the original post and since then. As I said in my original reply: “However, what you are asking for is not quite a debounce.”

I think we have established that debounce is not the correct solution here.

Yes, use tick(). I apologize about the confusion related to “global variable”, since most people thought of _G when I said that. I just meant a variable in the script, as opposed to a variable inside the scope of the event. As long as that is the case you are fine. I wouldn’t advise using _G for that.

1 Like

Okay so try a timer

local timer = 0
local buffAlreadyRunning = false

local function buff ()
    if not buffAlreadyRunning then
         buffAlreadyRunning = true
         while timer >= 1 do
           -- give boost
         end
         buffAlreadyRunning = false
    end
end


remote.OnServerEvent:Connect(function ()

    -- non yielding code

   timer = 10
   buff()
end)

You can do a setup like this, it stills requires a debounce though

This is the code for the idea I mentioend in my original reply. Based on your code, I’m guessing this is what you would like it to do, but let me know if it’s not. Someone else may have a better idea, but if I understand you right, this should work.

local lastTriggeredTime -- Our variable for tracking the last time a rune event was called

Remote.OnServerEvent:Connect(function(plr)
        local current_time = tick() -- Get the time
        -- Don't call tick twice; use the time got earlier so it always match the variable used earlier
        lastTriggeredTime = current_time 

	local char = plr.Character or plr.CharacterAdded:Wait()
	local HRP = char.HumanoidRootPart
	local runesFolder = char:FindFirstChild("Runes"):GetChildren()
	
	--Ignore this part, it's irrilevant
	if #runesFolder == 0 then
		print("Generate a rune first!")
		return
	elseif runesFolder[1]:GetAttribute("Type") == 1 then --Damage boost
		runesFolder[1]:Destroy()
		char:SetAttribute("activeRune",1)
	elseif runesFolder[1]:GetAttribute("Type") == 2 then --Regen energy
		runesFolder[1]:Destroy()
		char:SetAttribute("activeRune",2)
		
	elseif runesFolder[1]:GetAttribute("Type") == 3 then --Heal
		runesFolder[1]:Destroy()
		char:SetAttribute("activeRune",3)
	end

	--This is the part causing problems
	task.wait(10)
        if (current_time == lastTriggeredTime) then
             -- If there hasn't been any more calls since this one, and it's been 10 seconds, we can go back to no active rune
	         char:SetAttribute("activeRune","None")
        end
        -- If the current_time does not equal the lastTriggeredTime, then we know another rune is active and we don't want to cancel it. So, we do nothing.
end)
1 Like

You just need a loop in the place where you originally had

task.wait(10)

Instead of that, you need this:

repeat
  local leftToWait = 10-(now()-lastStarted)
  wait(leftToWait)
until now()>lastStarted+10 

lastStarted is the variable mentioned in the other posts.
As they said, you just have to declare it as a script-local in the outer scope, no need for globals.
And your trigger-event must of course update lastStarted.

Further, you probably also need a debounce of some sort.
So as long as your “10-second” loop is running, a debounce-flag should abort any attempts to re-enter the loop.

1 Like
local currentTime = tick() - 10 --negative 10 allows event to be fired from the get-go

Remote.OnServerEvent:Connect(function(plr)	
	local char = plr.Character or plr.CharacterAdded:Wait()
	local HRP = char.HumanoidRootPart
	local runesFolder = char:FindFirstChild("Runes"):GetChildren()

	--Ignore this part, it's irrilevant
	if #runesFolder == 0 then
		print("Generate a rune first!")
		return
	elseif runesFolder[1]:GetAttribute("Type") == 1 then --Damage boost
		runesFolder[1]:Destroy()
		char:SetAttribute("activeRune",1)
	elseif runesFolder[1]:GetAttribute("Type") == 2 then --Regen energy
		runesFolder[1]:Destroy()
		char:SetAttribute("activeRune",2)

	elseif runesFolder[1]:GetAttribute("Type") == 3 then --Heal
		runesFolder[1]:Destroy()
		char:SetAttribute("activeRune",3)
	end

	--This is the part causing problems
	local eventFiredTime = tick()
	if eventFiredTime - currentTime <= 10 then
		task.wait(10 - (eventFiredTime - currentTime))
	end
	currentTime = tick()
	char:SetAttribute("activeRune","None")
end)

This should cover what you’re looking for, this will allow the event to fire multiple times but will only execute the :SetAttribute() method in 10 second intervals, at minimum.

1 Like

Alright so it’s important to note that Luau is (for now) single-threaded, meaning code doesn’t execute in parallel. What task.wait does is essentially suspends the current thread, allowing others to continue.

There are a multitude of solutions to your problem, however the simplest one is simply to add a debounce to your code to prevent the server from triggering a new event during the 10 seconds the thread is yielded.

Alternatively, you could attempt to queue tasks, potentially by having any remote calls that occur during the waiting period store their arguments in an array to be fulfilled once the wait period is over. I should warn you, however, this invites the possibility of memory leaks on the client if remotes are called too frequently, which can have severe performance penalties if left unmanaged.

1 Like

Really sorry for the late reply, was a bit busy!
Thank you so much for the code, I was trying to figure out how to apply your method without any successful result.
I tested yours and it seems to work! However, I have a question: this script is placed inside ServerScriptService, meaning one script is used for all players. Considering how it seems to store data, wouldn’t that cause problems if multiple players were using the skill, since the variables would overlap between each player? If so, what’s the best fix? Using a module script and cloning it everytime a player uses the skill in order to mantain the variables separated for each player?

No worries, I’m glad it’s working!

That’s an excellent point; all of the clients would be using that same lastTriggeredTime. Correct me if I’m wrong, but it looks like the runes folder is inside the character and is being modified by local scripts. If that is the case, we can’t store the lastTriggeredTime inside the character since we don’t want clients touching this value.

So, we can use a dictionary (or table) to track the last time a player called the function. It’s perfectly fine to just have one script in ServerScriptService. We’ll add a player to the dictionary using the player added event, remove a player to the dictionary using the player removed event, and modify the dictionary value whenever the rune event is called.

local Players = game:GetService("Players")
 
local runeDictionary = {} -- This will keep track of the latest call times for all players
-- Our key will be the player's UserId, and the value will be the last time they called the rune event
-- Our variable for tracking the last time a rune event isn't needed anymore and is replaced by this runeDictionary

Players.PlayerAdded:Connect(function(player)
	runeDictionary[player.UserId] = 0
        --  Since it will never be time 0 again, this should be a safe value to initialize to
        -- nil has special meaning in dictionaries so we can't leave it blank or use nil here
end)
 
Players.PlayerRemoving:Connect(function(player)
	runeDictionary[player.UserId] = nil
        -- In dictionaries, setting a value as nil will remove it from the dictionary
end)

Remote.OnServerEvent:Connect(function(plr)
        local current_time = tick() -- Get the time
        -- Don't call tick twice; use the time got earlier so it always match the variable used earlier
        runeDictionary[plr.UserId] = current_time 

	local char = plr.Character or plr.CharacterAdded:Wait()
	local HRP = char.HumanoidRootPart
	local runesFolder = char:FindFirstChild("Runes"):GetChildren()
	
	--Ignore this part, it's irrilevant
	if #runesFolder == 0 then
		print("Generate a rune first!")
		return
	elseif runesFolder[1]:GetAttribute("Type") == 1 then --Damage boost
		runesFolder[1]:Destroy()
		char:SetAttribute("activeRune",1)
	elseif runesFolder[1]:GetAttribute("Type") == 2 then --Regen energy
		runesFolder[1]:Destroy()
		char:SetAttribute("activeRune",2)
		
	elseif runesFolder[1]:GetAttribute("Type") == 3 then --Heal
		runesFolder[1]:Destroy()
		char:SetAttribute("activeRune",3)
	end

	--This is the part causing problems
	task.wait(10)
        if (current_time == runeDictionary[plr.UserId]) then
             -- If there hasn't been any more calls since this one, and it's been 10 seconds, we can go back to no active rune
	         char:SetAttribute("activeRune","None")
        end
        -- If the current_time does not equal the lastTriggeredTime stored in the ruenDictioanry, then we know another rune is active and we don't want to cancel it. So, we do nothing.
end)
3 Likes