I want to understand, in more detail, what MonoBehaviour.StartCoroutine() does. I understand that it starts to iterate over a method returning IEnumerator but how exactly does it do this every frame?
When I try to circumvent the use of StartCoroutine(), and to use a silly example, I write something like the following.
...
private IEnumerator myenumerator;
void Start() {
myenumerator = GetEnumerator();
}
IEnumerator GetEnumerator() {
return MyIterator;
}
IEnumerator MyIterator() {
for(int i = 1; i < 4; i++) Debug.Log(i);
yield return new WaitForSeconds(2f);
}
void Update() {
if (Input.GetKeyDown("f")) {
while (myenumerator.MoveNext()) ;
}
}
When I press the key f the output is of course 1, 2, 3 in the console but it is all happening during the same frame. This is of course no surprise and this is exactly what the StartCoroutine() fixes and I am interested as to how, exactly, it does this.
Should I keep track of the number of frames occuring and only run myenumerator.Movenext() at consecutive frames? Or is there some more elegant solution here?
When you call StartCoroutine() Unity takes that IEnumerator and immediately calls .MoveNext() on it, before StartCoroutine() even returns to the calling site.
Based on what comes back from .MoveNext(), Unity choses when the next time to call .MoveNext().
That’s it, that’s all Unity does with Coroutines.
For instance, yield return null says “Call me on the next frame.”
Yield return new WaitForSeconds(X) will not call you again until X seconds have passed.
Additionally Coroutines are run in the context of a Monobehavior, which means if that Monobehavior goes away, gets disabled, or its underlying GameObject (or parent hierarchy) gets disabled, the Coroutine will no longer process.
More information on finer timing details can be found here:
There’s a great flowchart graphic to show you timing and ordering.
Yes this is the part of code I am interested to know more about. Because if you write it like I did in the example, the whole IEnumerator will be iterated over during one frame. How can I call my IEnumerator in Update() and have the .MoveNext() be halted for n frames?
That’s not really how coroutines are intended to be used in Unity. I’m fairly certain I have never in 13 years used “.MoveNext()” on a coroutine (not including stuff like iterating over lists, etc - I mean frame to frame coroutines) - let the engine handle that or it will behave oddly. If you want to do something like “only move forward in this coroutine when the F button is held”, write that into the coroutine. (Or, I suppose, avoid StartCoroutine altogether and just exclusively call MoveNext yourself?)
Yes that is what I am trying to do. What I fail to understand is how the StartCoroutine can call the IEnumerator in the Update() function and not have the whole IEnumerated iterated over during one frame.
This is implemented on the Unity side, so you’d have to duplicate the behaviour, using your own Update loop and probably a helper class.
In theory it’s easy - have a stack of IEnumerators, call MoveNext() once on the top IEnumerator every frame. If MoveNext returns false, pop the IEnumerator. If it retuns true, and the value of .current is another IEnumerator, push that new IEnumerator to the stack. . You’re done when the stack is empty.
What complicates this is that some of the built-in yieldInstructions have special behaviour that’s also implemented engine-side. If you get a WaitForSeconds or WaitForSecondsRealtime, you’ll have to note that and make timestamps. If you get a WaitForFixedUpdate or WaitForEndOfFrame, you have to note that and have special behaviour in your Fixed- and LateUpdate methods.
tl;dr: Probably don’t do this? Are you asking out of curiosity, or are you trying to learn something?
I was able to do what I wanted by calling the .MoveNext() every frame in the end. I was only playing around with it to understand coroutines in Unity better.