Countdown with a float inside Update() giving strange behavior on last iteration in C#

Edit: Please disregard this post. I found out there was a trivial answer: another part of my code was modifying the cooldown timer and after I got rid of it this started working correctly.

I have a C# script where the player can spawn fighters and there’s a cooldown timer between spawnings. After a spawn, the game shows a cooldown timer bar and I’m trying to make the bar start full, decrease, and ultimately disappear (using .SetActive(false)) when it reaches zero and you can spawn another fighter. In Update() I have an if() that sees if the cooldown is greater than zero; if so then it decrements the cooldown and then an inner if() checks if it fell to zero or less, and if it fell to zero or less then it deactivates the timer bar.

When I do that, everything seems to work except when the cooldown finally reaches zero or less the script doesn’t always deactivate the cooldown bar. Sometimes it does, and sometimes it doesn’t, which seems weird.

Even after a spawning when the cooldown bar doesn’t disappear, in the inspector I can see that the cooldown value has indeed dropped below zero and I’m able to spawn fighters again (the spawning code checks to make sure the cooldown is zero or less). I tried putting a Debug.Log within the final if() that checks to see if the cooldown reached zero or less, and the Debug.Log only gives a message if the cooldown bar does disappear. On spawnings when the cooldown bar doesn’t disappear, Debug.Log isn’t giving any message, so it looks like the problem is that the inner if() isn’t triggering even though it should. For the purposes of making my game, I could find another workaround like using an Invoke() to hide the cooldown bar while still letting Update() handle decreasing its size, but I get the feeling there’s something fundamental I’m not understanding about how C# and/or Update() and/or Unity is working that is making this approach so unreliable and I’d like to understand why it’s unreliable and if it has any implications on how to avoid similar pitfalls.

Here’s the code, the first block is what’s inside Update() and the others are methods that it calls:

    // If spawning is in cooldown, decrement the cooldown timer
    // When it reaches zero, hide the cooldown bar
    if (cooldown > 0f) {
      cooldown -= Time.deltaTime;
      SetCooldownBar();
      if (cooldown <= 0f) {
        Debug.Log("Attempting to hide cooldown bar");
        ShowCooldownBar(false);
      }
//  } else {
//    ShowCooldownBar(false);
    }

  void SetCooldownBar() {
    Vector3 cooldownBarScale = cooldownBarMax.transform.localScale;
    cooldownBarScale.x *= cooldown / cooldownMax;
    cooldownBar.transform.localScale = cooldownBarScale; 
  }
 
  void ShowCooldownBar(bool onoff) {
    cooldownBar.SetActive(onoff);
    cooldownBarMax.SetActive(onoff);
  }

I’ve tried changing the inner if() from “if (cooldown <= 0f)” to “if (!(cooldown > 0f))” and that doesn’t help.

I initially had the call to SetCooldownBar() within an else after the inner if() instead of happening right before the inner if(), and I still had the same problem of unreliable triggering of the inner if().

If I un-comment the else near the end of the block within Update() then the cooldown bar always disappears, but calling a function to hide the cooldown bar on every Update seems pretty wasteful (or at least it would make me look like an idjit of a programmer).

At first blush, the code looks fine. You really should show the entire code, as showing only excepts can easily hode the real bug. The most likely reason for the behaviour you describe is that there is some other place where you are changing the value of cooldown.

Try replacing cooldown <= 0f) with cooldown < 0f || Mathf.Approximately(cooldown, 0f)). The problem here is that floating-point numbers are inaccurate. 0.1f literally does not exist, for instance. You can read more about the reasons here, for instance: Why 0.1 Does Not Exist In Floating-Point (Exploring Binary).

That being said, if you check for equality, you need to check against a small range, a so-called tolerance or epsilon instead. Unfortunately, Mathf.Approximately (Unity Scripting API) has a very narrow tolerance, so you might need to write your own tolerance check. You can find one here, for instance: Mathf.Approximately does not work (Stackoverflow). I myself recommend Extension Methods (C# Programming Guide) for your own utility class.

I don’t think the issue here is a malformed equality test, the conditions look sound and safe to me. The approx() clause does not add safety to the evaluation at all, but potentially adds the possibility of a second execution of the innermost code in some cases:

  • Without approx, even if Zero equality fails, it won’t on the next Iteration; there is no possible path where this can lead to unexpected behaviour. The outer guard checks for > 0, and even if the decrement evaluates to exactly Zero, it would be caught by the second guard.
  • When you add the “OR Approx(…)” clause, that would also allow small values greater than Zero to pass, allowing a second subsequent execution of the innermost branch in the next iteration. Granted, that’s not catastrophic here, but adding the approx clause makes the code worse, with the potential of intermittend unexpected behaviour.

You’re exactly right. Soon after I posted that, I went back and looked through the code again and saw that’s what was going on. Unfortunately there was no way for me to delete the post after I realized that, all I could do was edit and say please disregard, but you probably saw it before I edited.

I imagined that people wouldn’t want to read through a ton of code to help diagnose an issue in a small subroutine and that posting a wall of code would guarantee that no one would respond, but if that’s where the real issue lies then there’s no way around it. Many thanks for looking over it.