How to stop a coroutine after condition is met?

Hey wonderful Unity friends! I’m new to coroutines but I think they will work well for my use case here. I’m just struggling to stop my first coroutine after the condition is met.

Here’s the goal:

  • Grow the object until y = 15
  • Once y = 15, halt the object growth and wait for 2 seconds
  • After 2 seconds, shrink object

But here’s what’s happening instead:

  • Object is growing smoothly until y = 15
  • Once y = 15, the object sporadically shrinks and grows
  • Both “grow” and “shrink” coroutines keep getting called together

Here’s the code I’ve written so far. It’s invoked after an object is instantiated with a click, which is why I’m not using the Start function. I’m thinking that Update might be part of the issue?

public class ExplosionScript : MonoBehaviour
{
    Vector3 scaleChange = new Vector3(.03f, .03f);

    public GameObject selectedObject;
   
    void Update()
    {

        if (selectedObject.transform.localScale.y < 15f)
        {
            StartCoroutine("Grow");
        }

        //Problem: debug.log tells me both "Grow" and "Shrink" are getting called even after condition is met:

        else if (selectedObject.transform.localScale.y > 15f)
        {
            StopCoroutine("Grow");
            StartCoroutine("Shrink");
        }

    }

    IEnumerator Grow()
    {
        selectedObject.transform.localScale += scaleChange;
        Debug.Log("growing");
        yield return null;
    }

    IEnumerator Shrink()
    {
        yield return new WaitForSeconds(2f);
        selectedObject.transform.localScale += -scaleChange;
        Debug.Log("shrinking");
        yield return null;
    }

As is, your coroutines are pretty useless. You might aswell put their lines of code into the if scope. Your current problem is completely unrelated to Coroutines whatsoever. You increase the size if it’s below 15, and you decrease the size if it’s above 15. You do that each frame, so effectively it will just twitch around 15.

What you want to do is alternate between a state in which the object grows until some point, then waits there, shrinks until some point, (waits there?) and repeats. You can achieve this behavior with or without Coroutines, however in both cases you must think about the actual logic involved. For example, you want a timer. There currently is none. You also dont want it to restart growing as soon as it hits a value < 15, and so on. It’s two separate states, either growing or shrinking. For more complex things you might wanna look up state machines in the future.

There is also two programmatic issues with your solution as well. First of all, it gets stuck if the value ever hits precisely 15.0f. Secondly, it runs dependant of the framerate. To fix the former, one of your conditions must be <= or >= instead of just < or >. To fix the latter you must always define changes per second, and multiply them with Time.deltaTime while applying them each frame.

As i said, you can realise this in a Coroutine as well as in Update() directly. In Update you need to check the time(r) yourself, Coroutine can just wait for 2 seconds. I usually advice against Coroutines, but it might be more convenient since you seem new. This would be an example in pseudocode, which assumes you only start it once and let it run:

while(true){
    while(size <= 15){
        // increase size and yield
    }
    // wait for 2 seconds
    while(size >= 0){
        // reduce size and yield
    }
    // depending on what you wait, wait for 2 seconds again
}
2 Likes

Yoreki, thank you for all of this helpful feedback!! A couple things I should have mentioned in the beginning:

  • I actually only want to run this sequence one time, and then destroy the gameobject. I was thinking Coroutines could help set up a “checklist” to make sure one thing follows the other: grow, pause, shrink, destroy.
  • I was initially trying an IF statement with this logic. This worked nicely, but I wanted to introduce that 2 second pause, and that’s how I got on coroutines.

Here was the original snippet I was working with, it worked well but didn’t have a pause:

    selectedObject.transform.localScale += scaleChange;

            if (transform.localScale.y< 1f || transform.localScale.y> 15f)
            {
                scaleChange = -scaleChange;

            }

I went and tried your code suggestions in my coroutine, but I may just be misunderstanding how the two states are being read by Unity here. It actually made my game crash, probably because both states are technically true at the same time? I’m going to keep exploring solutions here, and I will next look into adding a timer as you suggest!

Here’s the new Coroutine, it’s making my game crash but I think if I could fix the states it might work nicely:

   IEnumerator GrowHoldShrink()
    {
        float size = selectedObject.transform.localScale.y;
        while (true)
        {
            while (size >= 0)
            {
                Debug.Log("Start Coroutine");
                selectedObject.transform.localScale += scaleChange;
            }

            yield return new WaitForSeconds(2f);

            while (size <= 15)
            {
                Debug.Log("Shrinking");
                selectedObject.transform.localScale += -scaleChange;
            }
            yield return null;
        }
    }

The reason for the crash is that you’re never updating ‘size’. Once you assign ‘size’ to the local scale y, those two values become complete unrelated and updating the scale doesn’t update the value of ‘size’. Thus Unity is entering the loop and never leaving it.

You should look up the difference between value types and reference types.

You’ll either need to use the localScale.y in your condition, or to keep updating the value of size.

1 Like

What @spiney199 said is correct. I simply wrote size out of laziness, you would either have to update its value each time you adjust the localScale, or just write out the whole localScale.y thingy.

However, this is actually two problems in one. You also forgot to yield in the inner while loops!
Yield makes the Coroutine give control back to Unity. So we basically tell it to continue running next frame. Coroutines work with infinite loops (like the outer while true loop) as long as you give control back to Unity inside of it. So the real reason Unity crashes is the combination of accidentally creating an infinite loop there (by not updating size), and not yielding control back to Unity.

Examples:
With yield but without updating size: object grows infinitely!
Without yield but with updating size: object reached max size between two frames, ie instantly!
Without both: Unity crashes since it never gets control back from an infinite loop!
With both: We get a sort of animation by adding small changes each frame!

Talking about frames, you still need to make sure it runs independant of the framerate. So someone with 300 FPS wont have the animation complete faster than someone with 30 FPS. Currently the first person would complete your animation 10x faster.

All in all the inner loop(s) should look more like this:

while (size <= 15)
            {
                selectedObject.transform.localScale += scaleChange * Time.deltaTime;
                size = selectedObject.transform.localScale.y;
                yield return null;
            }

Since we multiply with Time.deltaTime, your scaleChange now defines the change per second. So you likely want to put a higher value than 0.03f. Since we update the value for size, the loop will eventually stop, and since we yield, it will only do one iteration per frame. So it will grow in size until it reaches the condition, then it reaches your WaitForSeconds, and then it enters the shrinking animation part, which you handle similar to the above.

If you dont want the animation to repeat simply remove the outer while(true) loop and destroy the gameobject after the second inner loop (the >= 0 one for shrinking).

Also pay attention to when you want to increase or decrease in size! In the code sniplet you posted, you decrease size while <= 15, while increasing while >= 0. This too means that the loop will never end! If we have a value >= 0 and add to it each cycle, it will never not be >= 0 anymore. That is why in my example you increase while you are below the cap and decrease while you are above zero. A loop always has to eventually make its own condition become False. Make sure to not create infinite loops :slight_smile:

1 Like

Oooh I’m so glad you said that, thank you! I looked it up and I now understand that I was making “size” a value type, which means it wasn’t updating when I was trying to make changes to it. I’m going to have to keep it a reference type.

I’ll keep looking into this, but so far super appreciate both of your help. I’ll post another update soon.

1 Like

Y’all, IT WORKED!! Thank you so much!! Here’s the final code snippet.

There’s still some odd behaviors going on. Sometimes the balls disappear smoothly, but sometimes they seem to go back through the start of the coroutine and grow/shrink a couple times before disappearing smoothly again. (I’m instantiating a bunch of balls when they collide with each other, so I think they could be somehow grabbing each others’ localscale values. I just need to figure out how to make each prefab independent of the others.).

Anyways, I think this works great, and I learned a lot about coroutines in the process. Thanks for all y’alls help! :slight_smile:

    void Update()
    {
        StartCoroutine("GrowHoldShrink");
    }

    IEnumerator GrowHoldShrink()
    {

        while (transform.localScale.y <= 20)
        {
            transform.localScale += scaleChange * Time.deltaTime;
            yield return null;
        }

        yield return new WaitForSeconds(2f);

        while (transform.localScale.y >= 0)
        {
            transform.localScale += -scaleChange * Time.deltaTime;
            yield return null;
        }

        Destroy(this.gameObject);

    }
1 Like

Why do you start a new coroutine every frame? That means you would have several coroutines running at the same time. Depending on the framerate you probably running several hundred coroutines simultaneously and each is modifying the scale of the object. You should start the coroutine once in Start. There’s no need for an Update at all.

2 Likes

You’re right!! I added it to Start instead of Update and it worked way better, in fact that seems to have fixed the weird issue where the balls occasionally grow and shrink again. Thank you!!