Yield return is breaking my brain

I’m not a stranger to async programming, but I hit a point where I am having a hard time intuiting how the logic flows in this one.

So in the new FadeOut function:

  1. Check if there is a coroutine running. If there is, stop it.
  2. Set the active coroutine to fade out
  3. yield return the current coroutine

That all makes sense. What doesn’t make sense is what happens when the unity back end goes through and continues the coroutines. We’ve returned a coroutine, but FadeOut was itself a coroutine that was called by the scene management somewhere. If I’m interpreting this right, on the second pass, since we returned a coroutine, will the logic just jump straight into the coroutine we returned? If that’s true, why does the FadeOutRoutine keep getting called when we yield return null? Does the coroutine only end when we don’t yield anything?

It’s hard to tell, really, because the code is in the C++ layer and not readily available.

Yes, like any other enumerable, the code returns to the last yield return it executed. However, FadeOutRoutine is not a Coroutine, per se, it’s an IEnumerator. If I had to hazard a guess, I’d say the StartCoroutine wraps the IEnumerator in a Coroutine class which it returns.

It will return to the position of the last yield it performed when accessed again. When we yield return null, we are not passing back a null coroutine. The Coroutine class will always have an instance, even if the code yield return null. It’s just another value of the enumerable. Like a list of objects where some objects are null. When we loop through the list it will, at some point, give us these null object and continue to do so until there is nothing left to give. It doesn’t pass itself back as a null object. Once the IEnumerator exits, the coroutine will end.

Internally, I don’t really know what’s happening. This is just how I understand these to work

2 Likes

Unfortunately, the exact code for the Coroutine class is buried inside of a DLL with little more than a wrapper class pointing to the external method.

The Coroutine is not a true async. It’s Unity taking advantage of a feature in C# (the IEnumerable and IEnumerator interfaces) to fake async operation (that’s important to know because if you don’t regularly yield return, an IEnumerator will be a blocking operation and your frame rate will suffer or your game may even stop responding!).

What happens is every frame any Coroutines attached to the component are given their slice of time. The yield return statement, which in a normal IEnumerable would be the type of the IEnumerable, is generally an object.

If that yield return is another IEnumerator, then execution on the current IEnumerator is paused until the next IEnumerator runs out of instructions. An example of this is

yield return new WaitForSeconds(1.0f);

The WaitForSeconds is itself an IEnumerator

public IEnumerator WaitForSeconds(float time)
{
    float elapsed = 0f;
    while(elapsed < time)
    {
         elapsed+=Time.deltaTime;
         yield return null;
     }
}

Effectively, WaitForSeconds just keeps running until the elapsed time is ran, yielding control every frame.

Notice that yield return null in there…

All that means is that when the Coroutine encounters a null it just says “oh, ok, we’re done for this frame” and control resumes in the next frame. In fact, here’s the code for WaitForEndOfFrame()

public IEnumerator WaitForEndOfFrame()
{
    yield return null;
}

That’s why most coders skip the instantiation of WaitForEndOfFrame and just use yield return null.

and finally

Exactly!

2 Likes

That makes a lot of sense and cleared up my confusion. Basically, we only relinquish control back to unity when we yield return, and if we don’t yield, it means the coroutine is all done. Thanks! It also helps to know that this is “fake async”, which was cause for a lot of my confusion.

1 Like

Privacy & Terms