Help with coroutine

Kind of embarrassed to post this, but I’ve done a lot of searching and can’t seem to get a clear answer as to what I’m doing wrong in my script. I’m trying to create a stamina script for my character, and I want the bar to fill up gradually as long as my character is not using stamina skills. Problem I run into is I’m using a coroutine with a while-loop, and for reasons I can’t figure out, the whole game freezes while the coroutine is running. It’s not an infinite loop, and the game does return to normal after the co-routine is finished. Any advice on how to fix this would be appreciated.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class StaminaBar : MonoBehaviour
{
    public float maxStamina, stamina, regenSpeedMultiplyer;

    bool regen = true;

    public GameObject player;
    public Slider slider;
    public Image fill;

    public void Start()
    {
        SetMaxStamina(maxStamina);
        stamina = maxStamina;
    }
    public void Update()
    {
        SetStamina(stamina);
    }

    public void SetMaxStamina(float stamina)
    {
        slider.maxValue = stamina;
        slider.value = stamina;
    }

    public void SetStamina(float stamina)
    {
        slider.value = stamina;

        if (regen == false || Input.GetButton("X") || Input.GetButton("R1") || stamina >= maxStamina)
        {
            StopCoroutine(RegenStamina());
        }
        else { StartCoroutine(RegenStamina()); }
    }

    IEnumerator RegenStamina()
    {
        print("regen start");
        regen = false;
        yield return new WaitForSeconds(2);

        while(stamina < maxStamina)
        {
            stamina += ((maxStamina * regenSpeedMultiplyer) / 100) * Time.deltaTime;
            slider.value = stamina;
        }
        regen = true;
        print("regen end");
    }
}

Put a yield return null; line inside your while loop.

3 Likes

Exactly this, and to explain why:

Coroutines are special, but they’re not that special. Like all the other code you write in Unity, they run on the main thread, and they completely hog up the main thread while they do so. The only time coroutines can “pause” their execution and let the rest of the game do its thing is when you use a yield return statement. yield return null just tells Unity “Ok I’m done with my work for this frame. Come back to me next frame.” Without that, your entire while loop runs during a single frame, and therefore you don’t get the “happening slowly over time” effect that you’re looking for.

2 Likes

Very good explanation! I had seen other threads talk about the yield return in while loops, but I guess I never pieced together what it was actually doing. Thanks for taking the time to educate me.

So that definitely fixed the issue I was having, but exposed another one. For reasons I don’t understand, triggering the StopCoroutine is not ending the while loop inside the coroutine. This makes it so stamina constantly recharges even while stamina is used until it is completely refilled. I’m trying to make sure it stops refilling any time a skill is used, and then has a two second delay before it begins recharging again. Any ideas on how to end the while loop if either “X” or “R1” are pressed? Updated code below.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class StaminaBar : MonoBehaviour
{
    public float maxStamina, stamina, regenSpeedMultiplyer;

    bool regen = true;

    public GameObject player;
    public Slider slider;
    public Image fill;

    public void Start()
    {
        SetMaxStamina(maxStamina);
        stamina = maxStamina;
    }
    public void Update()
    {
        SetStamina(stamina);
    }

    public void SetMaxStamina(float stamina)
    {
        slider.maxValue = stamina;
        slider.value = stamina;
    }

    public void SetStamina(float stamina)
    {
        slider.value = stamina;

        if (Input.GetButton("X") || Input.GetButton("R1") || stamina >= maxStamina)
        { StopCoroutine(RegenStamina()); }
        else if (regen == true)
        { StartCoroutine(RegenStamina()); }
    }

    IEnumerator RegenStamina()
    {
        print("regen start");
        regen = false;
        yield return new WaitForSeconds(2);

        while(stamina < maxStamina)
        {
            stamina += ((maxStamina * regenSpeedMultiplyer) / 100) * Time.deltaTime;
            slider.value = stamina;
            yield return null;
        }
        regen = true;
        print("regen end");
    }
}

Thanks for adding the explanation, it was late when I saw this and I was just getting ready to close my PC down for the night, but I thought supplying the solution might alone might at least be a start :slight_smile:

StopCoroutine only works if you pass a reference to the exact coroutine you want to stop. To do that, you have to save a reference to it in the first place:

    Coroutine myCoroutine = null;

    public void SetStamina(float stamina)
    {
        slider.value = stamina;
        if (Input.GetButton("X") || Input.GetButton("R1") || stamina >= maxStamina)
        {
             if (myCoroutine != null) StopCoroutine(myCoroutine);
        }
        else if (regen == true)
        {
            // save the coroutine so we can stop it later
            myCoroutine = StartCoroutine(RegenStamina());
        }
    }
1 Like

Once again, you saved my bacon. It’s obvious I still have some things to learn about coroutines. Thanks for all the help. I did make one small adjustment so that the coroutine would restart after being stopped. Posted below for anyone who runs across this thread in the future.

    Coroutine myCoroutine = null;
    public void SetStamina(float stamina)
    {
        slider.value = stamina;
        if (Input.GetButton("X") || Input.GetButton("R1") || stamina >= maxStamina)
        {
             if (myCoroutine != null) StopCoroutine(myCoroutine);

             //added
             regen = true;
        }
        else if (regen == true)
        {
            // save the coroutine so we can stop it later
            myCoroutine = StartCoroutine(RegenStamina());
        }
    }

In many cases (and I believe in this case), coroutines are simply not appropriate. Using them is possible but it results in an awkward and brittle implementation.

Instead for something like this the a better way is a simple timer, nothing more.

float timeSittingStill;

When you are running, keep that set to zero.

if (running)
{
   timeSittingStill = 0;
}

Every Update(), without regard, always do

timeSittingStill += Time.deltaTime;

regen = false;
if (stamina < maxStamina)
{
  if (timeSittingStill >= 2.0f)
  {
    regen = true;
    stamina += RegenerationRatePerSecond * Time.deltaTime;
    if (stamina >= maxStamina)
    {
        stamina = maxStamina;
    }
  }
}

See how there’s no two paths of execution going?

See how as soon as you start running it stops regenning?

See how when you stop running it needs 2 full seconds before regen?

It’s all super super super simple logic when written this way.

Coroutines have a place, but in my experience 99% of the time they are misused and inappropriate for the situation at hand. And when misused, coroutines have a tendency to tie your brain into weird logic traps and loops and result in quirky bugs that are hard to discover and fix. Keep it simple top-down linear logic.

1 Like