Coroutine Behaving Weirdly

I have a basic coroutine that basically just reverses an animation. But it’s behaving very strangely…
Here’s my coroutine,

    IEnumerator SpeechBubbleDisappear(int spriteIndex)
    {
        float delay = anim.GetCurrentAnimatorStateInfo(0).length;
        yield return new WaitForSeconds(delay);
        image.sprite = sprites[spriteIndex];
        yield return new WaitForSeconds(10);
        image.sprite = sprites[sprites.Length - 1];
        anim.SetFloat("direction", -1);
        anim.Play("bubbleAnim",-1,float.NegativeInfinity);
    }

}

So, I can see it change sprites after the first “delay” it seems to work just fine up until there, but for some reason, it waits 10 seconds, changes the sprite again, and then waits another 10 seconds?? It’s as if I had another WaitForSeconds(10) call after the sprite gets changed, but I don’t. I tried testing it out by fiddling with the value, if I put it to 2 seconds, it will wait 4, if I put it to 10, it will wait 20. I don’t understand it.

Does someone know what’s going on here? I don’t have this coroutine being called anywhere else, it gets called from another method at the start of the game. I even added a StopAllCoroutines() call before calling it to ensure it can only be running one at a time, but it’s still doing it.

I think I don’t really understand your description of your problem. Your coroutine has an initial wait time which is based on the length of the current animation. After that initial delay (we don’t know how long that is) you change the sprite once to the sprite at “spriteIndex”. Now you wait 10 seconds. After that time has passed you switch the sprite again to the last sprite in the sprites array.

So your coroutine does set the sprite exactly 2 times. Are you sure you actually start that coroutine once? The fact that the method has a parameter for which sprite to display suggests that this coroutine may be called more than once?

Keep in mind once started a coroutine does run on its own (unless you destroy or deactivate the gameobject the coroutine runs on). Whenever you use StartCoroutine again you will start another seperate copy of the coroutine which will run in parallel.

You wrote:

What you described here is exactly what your coroutine should do except the last thing. You said it “waits another 10 seconds”. Well what exactly happens after those 10 seconds because your description ends here. The coroutine has finised after the last yield.

I think you should work on your description of what you observed and what is different from what you expect. Coroutines work reliably. If they don’t for you there’s probably something missing. You either start the coroutine multiple times, maybe on / from a different script / instance or you somehow messed with your timescale.

So, it’s basically a speech bubble, depending on what int is passed in the sprite will change to a different bubble.

What’s supposed to happen is the speech bubble is a blank bubble, it plays a popping up animation(about 1 second) after it finishes popping up, it changes to the respective sprite(I understand I could just have a text object rather than a bunch of sprites, but this is how my client wanted to do this lol) and then it’s supposed to wait for 10 seconds, then change back into the blank sprite and play the same popping up animation in reverse.

Here’s what happens, it does everything up until the blank sprite(at line 7), where it does switch the sprite, but then it waits another 10 seconds before playing the animation at line 8 and 9. I have tried adding a bool to make sure the coroutine cannot run twice, if bool == true, don’t run coroutine, and set bool to true in coroutine. I also added a StopAllCoroutines() in the method that calls this coroutine, and confirmed it is only calling the coroutine once via prints.

This is my entire script.

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

public class SpeechBubble : MonoBehaviour
{
    Animator anim;
    [SerializeField]
    Sprite[] sprites;
    Image image;
    public bool speechBubble;


    private void OnEnable()
    {
        image = GetComponent<Image>();
        anim = GetComponent<Animator>();
    }

    public void SpeechBubbleDisplay(int sprite)
    {
        image = GetComponent<Image>();
        anim = GetComponent<Animator>();
        StopAllCoroutines();
        StartCoroutine(SpeechBubbleDisappear(sprite));

    }

    IEnumerator SpeechBubbleDisappear(int spriteIndex)
    {

        anim.SetFloat("direction", 1);
        anim.Play("bubbleAnim");
        float delay = anim.GetCurrentAnimatorStateInfo(0).length;
        yield return new WaitForSeconds(delay);
        image.sprite = sprites[spriteIndex];
        yield return new WaitForSeconds(10);
        image.sprite = sprites[sprites.Length - 1];
        anim.SetFloat("direction", -1);
        anim.Play("bubbleAnim",-1,float.NegativeInfinity);

    }

}

It is only called once in my other script, on awake.

I don’t know what other information to provide, I’ve never had this happen before.

What happens if you add Debug log statements? That will prove that it is not running twice.

What happens if you comment out line 39 that sets the sprite image after the wait? Does it still wait an extra 10 seconds, or does the issue seem to be “solved”? Seeing what happens then should help with debugging and possibly help with a fix.

Also, out of curiosity, what happens if you add a “yield return null;” after the last line in that method. I know that shouldn’t matter.

Yeah, I added a print and it only printed once.

And I tried both those things, it still waits 20 seconds… With this coroutine,

    IEnumerator SpeechBubbleDisappear(int spriteIndex)
    {
        anim.SetFloat("direction", 1);
        anim.Play("bubbleAnim");
        float delay = anim.GetCurrentAnimatorStateInfo(0).length;
        yield return new WaitForSeconds(delay);
        image.sprite = sprites[spriteIndex];
        yield return new WaitForSeconds(10);
      //image.sprite = sprites[sprites.Length - 1];
        anim.SetFloat("direction", -1);
        anim.Play("bubbleAnim",-1,float.NegativeInfinity);
        yield return null;

    }

Very bizarre.

Next I might recommend trying to comment out the “yield return new WaitForSeconds(10);” and see if there is zero wait afterwards, or just 10 seconds now.

Then, what happens if you replace the first wait’s “delay” variable with a number (different than 10); and see if the behavior of everything changes at all.

The debug log should have caught this idea as well with duplicate log messages, but for funsies I want to mention it: So we covered that the coroutine is not being launched twice within the same mono, but is it possible that a second GameObject also has this attached to it. This might cause some weirdness like this if we expect this Mono to be a Singleton, but it is accidentally not being used as one. Especially if there are some static variables somewhere involved in all this. But I don’t see that being a factor from what code is supplied. Just spit-balling on this one.

If there is any way you can set up a simple project that maybe has the bare minimum assets and code files to make this reproduce, people might be able to help debug it better.

I just tried commenting it out, it just pops up then instantly goes back down lol. It is very confusing… If it was duplicate scripts on the same object it would also print twice.

And I tried changing the delay variable to 5, it waits 5 seconds, then 10, then 10.

Okay, I can upload the project, it’s not a very big project, but sorry I couldn’t shrink it down, i don’t know how it’s 500mb lol

Sounds good. I will take a look tonight after work.

1 Like

Thank you :slight_smile:

I’ve got a hacky workaround that isn’t a true fix, but it should accomplish the behavior you are intending until we can do a true fix here.

    IEnumerator SpeechBubbleDisappear(int spriteIndex)
    {
        anim.SetFloat("direction", 1);
        anim.Play("bubbleAnim");
        float delay = anim.GetCurrentAnimatorStateInfo(0).length;
        yield return new WaitForSeconds(delay);
        image.sprite = sprites[spriteIndex];
        yield return new WaitForSeconds(5f);
        anim.SetFloat("direction", -1);
        anim.Play("bubbleAnim", -1);
        yield return new WaitForSeconds(5f - delay);
        image.sprite = sprites[sprites.Length - 1];
    }

I’ve predicated the hacky temp fix based on two things we know about this:

  1. First, it is doubling the time the speech bubble plays. The WaitForSeconds() is playing, and the thread hits “anim.Play(“bubbleAnim”, -1);” and waits the same amount of time before playing the animation. The thread is hitting the anim.Play method exactly as it should after 5 seconds (in the above code). That method is being invoked, but something in the anim.Play logic is bugging out and waiting exactly as long as the previous wait did.
  2. We don’t want the sprite to disappear halfway through the process, and a blank bubble to be rendered for half the time the bubble appears.

So, I decided to trigger the Play logic, and then wait within the current coroutine again for the same period of time as before, minus the original delay period. Now, the speech bubble appears, the sprite/text appears after about 1 second (delay variable), The speech bubble and text render on the screen for about 10 seconds, the animation is triggered in reverse halfway through the 10 second wait but it does nothing for another 5 seconds, and then we trigger the empty sprite and close the speech bubble.

We now have the speech bubble operating the way I am pretty sure you want it. If you want the empty sprite to disappear at the same time as the animation plays, simply delete the “- delay” from “5f - delay”.

I will keep looking into the cause though. But this should work until then. Let me know if it helps for now.

1 Like

Yes, it’s a bandaid fix, but it works! Thank you. I really wonder what could be causing it… Ay, the joys of game design, when you think “Oh! I got a way to figure out what’s happening!” and try everything you know and still can’t find the issue. lol

1 Like

Instead of using debug logs add breakpoints and attach the debugger, then you will see what actually happens.

Breakpoints? as in, break;? How would that work?

Which IDE are you using? I assume Visual Studio?

Go to the line(s) you want to debug, for example line 5 and 7 in your code example and add a break point in your IDE
In Visual Studio the shortcut to toggle breakpoints is F9, a red circle icon should be shown next to the icon, that’s the breakpoint

Then inside Visual Studio click the “Attack to Unity” button (it maybe asks you which Unity Editor, select your local, running Unity Editor)

Now press play inside the Unity editor, your code will stop when you hit a breakpoint inside Visual Studio, then you can debug your code
Use shortcuts (F10, F11 etc) or the toolbar to run the code step by step then you can see what happens.
Press F5 to continue the code (until the next breakpoint)

While debugging you can toggle the breakpoints line again with F9 to add/remove breakpoints

Once you are done you can stop the debugger inside Visual Studio and the Unity player