Could someone explain this coroutine to me?

private IEnumerator StartupManagers() {
    foreach (IGameManager manager in startSequence) {
        manager.Startup();
    }

    yield return null;

    int modulesTotal = startSequence.Count;
    int progress = 0;

    while (progress < modulesTotal) {
        int modulesReady = 0;

        foreach (IGameManager manager in startSequence) {
            if (manager.status == ManagerStatus.started) {
                modulesReady++;
            }
        }
 
        if (modulesReady > progress) {
            progress = modulesReady;
            Debug.Log($"Progress:  {progress}/{modulesTotal}");
        }
        yield return null;
    }
 
    Debug.Log("All managers started up.");
}

This code is in a book I’m studying. I’m told I can use this to load large amounts of saved data, and this will give me the ability to, for example, display a progress bar at the same time. I thought I understood how coroutines worked from previous chapters, but this one has me puzzled a bit. There’s two “yield return” statements in the code, which I think is making it harder for me to really picture what is going on. Especially the first one. What am I actually yielding for for the first “yield return”?

The second “yield return” I think I understand… every frame we check the progress (by going through all of the managers again and counting how many have been started) and display that.

First time you call this it will only prepare/start up the managers.
Every subsequent time you call this, it will proceed with the actual behavior and will yield after each module, until you end up with the final message.

1 Like
private IEnumerator StartupManagers() {
    Debug.Log("A");

    foreach (IGameManager manager in startSequence) {
        manager.Startup();
    }

    yield return null;

    int modulesTotal = startSequence.Count;
    int progress = 0;

    while (progress < modulesTotal) {
        int modulesReady = 0;

        foreach (IGameManager manager in startSequence) {
            if (manager.status == ManagerStatus.started) {
                modulesReady++;
            }
        }

        if (modulesReady > progress) {
            progress = modulesReady;
            //Debug.Log($"Progress:  {progress}/{modulesTotal}");
        }

        Debug.Log("B");
        yield return null;
    }

    Debug.Log("C");
    // Debug.Log("All managers started up.");
}

If you had 3 managers (in startSequence) this would print out

A
B
B
B
C
1 Like

Are you saying that the first “yield” is being used to delay progress in the code for one frame? And then we move on to line #8 with “int modulesTotal = startSequence.Count;”? If that’s the case, what is the point of waiting a frame before continuing? I still don’t quite understand the use of the first “yield return null”, I’m sorry.

That’s helpful. You wrote this as I was replying. Could you still look at my previous reply? Thanks so much!

Well I wouldn’t say delay, it just yields execution.

The way to think of enumerators and yielding is like there is a magic pointer moving over the function. Imagine the function is able to remember where it was between the calls.

yield is used to denote the exact places where the function will abandon its execution and yield control back to the system. The next time you call it, it resumes. That’s the whole logic behind it.

Now in this particular example, the designers thought it was a good idea to include an initialization step, to “warm up” the managers in one go. What you get is what I describe in post #3

1 Like

Would the code function the same, and just as well, without the use of the first yield instruction?

Yes. I’m not entirely sure why they’ve chosen to do this. Maybe to illustrate how to prepend a pass.

1 Like

Thanks so much! Really appreciate the help!

1 Like

If you observe how enumerators are used in the wild, they’re typically some sort of compact state machines.
You can easily draw a block diagram out of this, with steps A, B, C, and hook up a loop between BxC and AxB.

1 Like

What book is this?

I would have a word with the author regarding this code. It makes me question whether the author should have written a book on Unity to begin with. For instance this manager.status is unnecessary and just complicates the code. And the idea of loading serious amounts of data this way is terrifying. Imagine you have 1000 modules - no matter how much time each one takes to initialize, you‘d already have a load time of 1000/60 or nearly 17s just for the coroutine to complete the sequence even if each module initializes in under 1 ms.

Normal folks would just enumerate the list of modules to initialize, call a method on the module, then yield. Pros would then count the time spent on loading so far in the current frame and only yield when it passed a certain threshold such as 30 ms. Seriously, the above code looks like teaching beginners bad code.

Correction:
The code hides its intention well. I now assume that the modules themselves run coroutines and this code fragment just checks whether they are all done. In that case the coroutine should have been replaced with an event system with a OnLoadFinished callback to the script that runs this coroutine. The script could then increment a counter and update the progressbar every Update. Way easier and does not require writing contrived coroutine code.

Sure there are better ways to make this happen. But I would argue that you got this backwards. The book is trying to illustrate coroutines (or enumerators) in a more general way, and the scenario is probably made up only to back up this illustration.

What book failed to do is to explain the actual dynamic of the yield keyword. Whatever language it used, didn’t prepare the reader to such an example.

You lost me there, lol. New programmer here. Taught myself C last year, C# Nov 2012, and started Unity a couple months ago.

Unity in Action, Third Edition

P.S. I changed the original code a little bit. Besides name changes (I wasn’t sure if I’d get in trouble for posting his actual code) I changed some logic. Here’s his original logic. Doesn’t mine make more sense? I know it’s just a small change, but still…

private IEnumerator StartupManagers() {
    foreach (IGameManager manager in startSequence) {
        manager.Startup();
    }
    yield return null;
    int modulesTotal = startSequence.Count;
    int modulesReady = 0;
    while (modulesReady < modulesTotal) {
    int progress = ModulesReady;
        modulesReady = 0;
        foreach (IGameManager manager in startSequence) {
            if (manager.status == ManagerStatus.started) {
                modulesReady++;
            }
        }
        if (modulesReady > progress) {
            Debug.Log($"Progress:  {progress}/{modulesTotal}");
        }
        yield return null;
    }
    Debug.Log("All managers started up.");
}

Finite state machines

1 Like