Completely restarting a loop

I’m not really sure how to describe it, but some background information would probably help. I have a powerup system where when a powerup is collected it ticks down in seconds until the time runs out and cancels that powerup. It gets a little more complex where if you pickup a powerup of the same type that you already have active, it resets the timer. The only issue with resetting the timer is it doesn’t fully restart the loop.

Here’s the code:

while true do
	wait(1)

	for powerupType, timer in pairs(ClientState.PowerupData) do
		if timer <= 0 then
			ClientState.PowerupData[powerupType] = nil
			coroutine.wrap(Powerups[powerupType].Stop)()
		else
			ClientState.PowerupData[powerupType] -= 1
		end
	end

	ClientState:Changed("PowerupData")
end

As you can clearly see, if the powerup timer were to reset right before the while loop was to run the next iteration, you’d basically lose an entire second in the worst case. To fix this I’d need to use a completely different structure/method of doing this. What should I go with?

What happens if you put the wait at the end of the while loop?
That should execute the code and then perform wait(1).
Would you still lose that second of PowerUp time?

I personally think using wait should be avoided.

I mean I have to assume that when you pick up the powerup it would override the previous data value.

local powerupTable = {
   ["Powerup1"] = 10,
   ["Powerup2"] = 20,
}

meaning that when someone picks up a powerup

powerupTable["Powerup1"] = 20 -- overrides the previous entry
print(powerupTable) -- {Powerup1 = 20, Powerup2 = 20}

The solution I would propose is not using dictionaries and just using a normal array.

local powerupTable = {
    {"PowerupType", 10},
    {"PowerupType", 20},
}

then to add

table.insert(powerupTable, {"PowerupType", 20}

ofc you would need to restructure the code depending on the dictionary.

So you’re suggesting creating some sort of cache to represent whenever a player picked up a given powerup? Maybe I’m not understanding the full extend of your answer, but even if you cached all the powerups a player has picked up and used the most recent one, you’ll still run into the same issue.


@k_as
Changing the location of the wait doesn’t affect anything besides running the calculation in the loop immediately when the loop starts.

kinda, I’m not entirely sure what you’re fully trying to accomplish, but if you don’t mind elaborating a bit more that would be appreciated.

What I think might be happening is something regarding iteration, eg: when you use pairs it will go through the table in a random order. Since you said the problem is when you pickup a powerup of the same type. But that’s just speculation.

Maybe this works?

local powerupData = {
	jumpBoost = 5,
	healthBoost = 10
}

--Player picks up a jumpBoost which adds 10 seconds:
powerupData["jumpBoost"] += 10

while true do
	wait(1)

	for powerupType, timer in pairs(powerupData) do
		if timer <= 0 then
			powerupData[powerupType] = 0
			--coroutine.wrap(Powerups[powerupType].Stop)()
		else
			powerupData[powerupType] -= 1
		end
	end

	--ClientState:Changed("PowerupData")
end

Instead of removing the powerup from the table you could just not activate it if it’s at 0.

There are different types of powerups and each type of powerup (at least in the current system) has it’s own value/timer. Every second the code goes through any active powerups and decreases the value or ends the active powerup. The issue comes when collecting a powerup when you already have that same type active. It resets the timer value back to the beginning value, the loop doesn’t account for this reset.

This means that if the loop just finished waiting and started the next iteration right after that number was reset, the timer would get decreased meaning you lose some duration of the powerup. Ideally, the loop for a given powerup type would restart when the timer is reset, but that’s what I’m trying to figure out.


@k_as
This also wouldn’t change anything besides keeping a given powerup type in the powerupData dictionary. The reason I delete the powerup from the table is to keep the loop from evaluating it after it finished.

Replying to your reply to @kingerman88.
Why don’t you give the while loop a different condition?

You could end the entire loop and start a new one by putting the while loop in a function and calling that whenever someone picks up a powerup. It’s a bit messy but this would technically do what you’re asking for.

Obviously ending the first while loop before starting a new one again.

connect a handler to render stepped, store end times in the powerupData like:

powerupData[powerupType] = tick()

if powerups do not have a set duration you can also do

powerupData[powerupType] = {tick(), DURATION}

where DURATION is a constant float.

Then you’d just do something like:

game:GetService("RunService").RenderStepped:Connect(function()
  local t = tick()

  for powerupType, startTime in next, powerupData do
    -- compare t to startTime, doing something like:
    if (t - startTime) > DURATION then
      powerupData[powerupType] = nil
    end

    -- if you did the second form, you'd have value instead of startTime above, and then:
    local startTime, duration = unpack(value) -- replace startTime in the for loop with value
    if (t - startTime) > duration then
      powerupData[powerupType] = nil
    end
  end
end)

Note: you need to use Stepped if this is on the server, like:
game:GetService("RunService").Stepped:Connect()

If you want, you can also put the loop into a function, and when it resets, you break the loop and call the function.

2 Likes

I think this would solve the timing issue. The one other problem presented by the old system is ClientState:Changed("PowerupData"). This is called to tell other code that the state of the powerups has changed and part of that is the GUI rendering of the timer which should display the amount of seconds left.

If I’m correct, this shouldn’t cause any problems besides updating the GUIs quite frequently. It would take the information about how much time is left in the powerup duration and calculate seconds and display it.