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.
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.
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
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.
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.");
}