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.
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.
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)
_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.
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.
Yeah, I think I explained myself already… 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.
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.
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)
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.
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.
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.
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?
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)