- Home /
Coroutines IEnumerator not working as expected
I am trying to stop/play one coroutine without luck, i simply create a IEnumerator variable, initialice the variable and try to start/stop the coroutine, but i am not getting the expected behaviour, i have created a simply code to reproduce my issue, i would expect the first coroutine to get stopped, start-finish again and start-finish one last time, but the result is the first coroutine finishes and no other coroutine gets called The expected log in the console should be start - start - end - start - end but the log is start - end @Bunny83 i am missing something?
public class TEST : MonoBehaviour
{
IEnumerator timeOutCoroutine;
IEnumerator Start()
{
timeOutCoroutine = TestRoutine();
StartCoroutine(timeOutCoroutine);
yield return new WaitForSeconds(1f);
StopCoroutine(timeOutCoroutine);
StartCoroutine(timeOutCoroutine);
yield return new WaitForSeconds(20f);
StartCoroutine(timeOutCoroutine);
}
IEnumerator TestRoutine()
{
Debug.Log("Start");
float timer = 0;
while (timer < 10)
{
timer += Time.deltaTime;
yield return null;
}
Debug.Log("End");
}
}
-----------------------------------UPDATE--------------------------
i have added some logs to the code to see if the rest starcoroutine gets called
yield return new WaitForSeconds(1f);
Debug.Log("FIRST LOG");
StopCoroutine(timeOutCoroutine);
yield return new WaitForSeconds(1f);
Debug.Log("SECOND LOG");
StartCoroutine(timeOutCoroutine);
yield return new WaitForSeconds(20f);
Debug.Log("THIRD LOG");
StartCoroutine(timeOutCoroutine);
the log result is: start - FIRST LOG - SECOND LOG - end - THIRD LOG
Try making the IEnumerator part for your Start method read private void Start() ins$$anonymous$$d :)
Completely Replace the word: IEnumerator Start() with: private void Start() If you have problems after that, then we'll go from there :) Cheers bud
Hello, Thanks for the suggestion but didnt work same issue test code, i am also updating the first question to give extra information to the issue please read it if posible :) ->
IEnumerator timeOutCoroutine;
void Start()
{
timeOutCoroutine = TestRoutine();
StartCoroutine(Initialize());
}
IEnumerator Initialize()
{
StartCoroutine(timeOutCoroutine);
yield return new WaitForSeconds(1f);
StopCoroutine(timeOutCoroutine);
StartCoroutine(timeOutCoroutine);
yield return new WaitForSeconds(20f);
StartCoroutine(timeOutCoroutine);
}
IEnumerator TestRoutine()
{
Debug.Log("Start");
float timer = 0;
while (timer < 10)
{
timer += Time.deltaTime;
yield return null;
}
Debug.Log("End");
}
Answer by Bunny83 · Jul 09, 2020 at 03:41 PM
:) I'm kinda suprised that until now nobody has asked a question like this. Even though I have explained what a coroutine actually is several times in other questions it just shows that there is still quite a misunderstanding what a coroutine actually is.
Lets just step back for a moment. Unity implements coroutines by utilizing a C# feature called "iterator blocks" or iterator methods (which are more generally known as Generator methods). C# ships with two relevant interfaces (IEnumerable and IEnumerator) as well as the special keyword "yield".
IEnumerable / IEnumerable<T>
An IEnumerable simply represents an object that can be iterated and can produce a sequence of "values". This interface is extremely simple and only defines a single method:GetEnumerator()
. Calling this method creates a new instance of an iterator object that actually implements the sequence. IEnumerable can be seen as a "wrapper" for the actual IEnumerator. The main benefit is that the IEnumerable can essentially store the originally passed parameters / arguments internally and you can produce as many iterator objects you want without the need of specifying the same parameters again.
Note that the difference between the generic version and the "normal" non-generic version is that, as we will see in the next section, the actual "values" we are getting back are either just of type System.Object / object or that we actually get a specific type-safe collection of values
IEnumerator / IEnumerator<T>
This is the actual heart of the whole deal. This interface provides two main things that can be used to "iterate" over the sequence we are interested in. The only relevant things are: the parameterless methodMoveNext()
as well as the readonly property
Current
. That's all.
The method MoveNext has a return type of bool
which indicates if it successfully moved to the next item in the collection. The Current property can be used to "read" the current value / item. As mentioned above this is where the generic type parameter comes into play. In the normal / non generic interface the type of this property is just "object" while in the generic version it has the type T
.
Just for completeness the IEnumerator interface also implements a parameterless method called Reset()
which is meant to "reset" the internal state of an iterator. What that exactly means is not really specified and I haven't really come across any usage of iterator blocks where this method is actually used. Also auto generated iterators that has been created through the use of the yield keyword do never implement this method because the compiler can never really determine what a reasonable reset would look like.
Usual usage of IEnumerables / IEnumerators
As we already mentioned those generally represent things you can "iterate over". If C# we have theforeach
loop which is somewhat related to those interfaces. When you have an object that implements the "IEnumerable" interface you can just use it in a foreach loop like this:
foreach(MyType val in myObject)
{
// do something with "val".
}
This actually translates to something like this:
var enumerator = myObject.GetEnumerator();
while (enumerator.MoveNext())
{
MyType val = (MyType)enumerator.Current;
// do something with "val".
}
(Note I simplified it a little bit. A foreach in addition ensures that Dispose is called if the object implements IDisposable, but that's irrelevant for now).
The key here is that our original object provides a method (GetEnumerator) to create a new iterator object. This iterator object is used by the compiler to actively "iterate" through that iterator object by calling MoveNext until it returns false which indicates the end of the collection. After each call to MoveNext the Current property should return the "current value".
The yield keyword
This is where the compiler magic comes into play. Of course we can simply create our own classes / types which implement the IEnumerable / IEnumerator interfaces and implement our own logic for the MoveNext method. However the yield keyword is a powerful piece of compiler magic. Whenever you use the yield keyword inside a method a drastic change happens. First of all the method must have either IEnumerator or IEnumerable (or the generic equivalents) as return type. Any other return type is not accepted. Further more the compiler will generate a hidden internal class for you that will actually implement your "code".I try to give you an idea what is happening. Imagine a simple example like this:
IEnumerable<int> MyIterator(int aFrom, int aTo)
{
for(int i = aFrom; i < aTo, i++)
yield return i;
}
This harmless looking "method" will turn into two classes and a method which will look something like this:
IEnumerable<int> MyIterator(int aFrom, int aTo)
{
return new ___MyIterator_Enumerable(aFrom, aTo);
}
private class ___MyIterator_Enumerable : IEnumerable<int>
{
int m_aFrom;
int m_aTo;
public ___MyIterator_Enumerable(aFrom, aTo)
{
m_aFrom = aFrom;
m_aTo = aTo;
}
public IEnumerator<int> GetEnumerator()
{
return new ___MyIterator_Enumerator(m_aFrom, m_aTo);
}
}
private class ___MyIterator_Enumerator : IEnumerator<int>
{
int m_aFrom;
int m_aTo;
int m_State;
int m_Current;
int m_i;
public ___MyIterator_Enumerator(aFrom, aTo)
{
m_aFrom = aFrom;
m_aTo = aTo;
m_State = -1;
}
public int Current
{
get {
if (m_State != 0)
throw new SomeException();
return m_Current;
}
}
public bool MoveNext()
{
switch(m_State)
{
case -1:
m_State = 0;
m_i = m_aFrom;
if (m_i < m_aTo) {
m_Current = m_i;
m_State = 0;
return true;
} else [
m_State = 1;
return false;
}
break
case 0:
m_i++;
if (m_i < m_aTo) {
m_Current = m_i;
return true;
} else [
m_State = 1;
return false;
}
break;
case 1:
default:
return false;
}
}
public void Reset()
{
throw new NotImplementedException();
}
}
This is a quite drastic change. Our original method does not contain any of the code we have actually typed in the body. Instead the body of our method has been torn apart and turned into a statemachine object. This generated class is hidden. You won't see this code anywhere unless you decompile your code into IL then you will actually find that internal class.
What's important to realise is that our method does no longer execute the code we've written but just creates an instance of a class. So it creates an instance of that iterator class. Note I specifically made an IEnumerable example just for completeness. If you specify "IEnumerator" as return type that IEnumerable wrapper class just doesn't exist and our method would directly return the IEnumerator class instance instead.
How Unity uses iterators to implement coroutines
Iterators or generators are mainly meant to iterate over / "generate" values of some kind. However since the C# compiler can magically turn our linear code into a statemachine that splits our code apart at the points where we yield a value, we can actually use that "method to statemachine" magic to implement coroutines.
While in "normal" iterators we are usually interested in the values that are produced by the iterator, we as developers are more interested in the side effects that is in between those yield instructions. When you "call" your coroutine method you don't call any of your code. Keep in mind that all you do is creating a new instance of that statemachine object that the compiler generated for your method. You now pass this object instance to the StartCoroutine method. This does two things at once:
First Unity will create and instance of it's internal "Coroutine" class to managed your coroutine internally and store that iterator object that you passed to StartCoroutine alongside for later use. In addition it will immediately call MoveNext once on that object. This "advance" your statemachine to the next yield statement. The scheduler then will look at the value that you yielded by examin the Current property. Based on that value the scheduler decides what to do next. We don't know the exact implementation of Unity's scheduler since it's implemented in native C++ code. However it most likely will just add that coroutine to a certain "waiting queue". So when you yield "null" the scheduler probably just adds the coroutine to a list of coroutines that should be resumed the next frame. When you yield "WaitForEndOfFrame" it just stores that coroutine in a list that gets processed at the end of the frame.
Each time a coroutine is "resumed" the scheduler simply calls MoveNext and again checks the yielded value when the coroutine wants to be resumed. Of course as soon as MoveNext returns false the coroutine / iterator has finished (either reached the end or a yield break;
was reached).
Conclusion
Hopefully this clears up the confusion what is happening in your case. Though just in case I will give a quick run-through:
In Start you call your coroutine to create a new statemachine instance. This instance is stored in your "timeOutCoroutine" variable. When you call StartCoroutine(timeOutCoroutine);
you pass your statemachine to the Unity scheduler and ask it to start a new coroutine. The scheduler will happily call MoveNext on your object and queue the coroutine whereever it needs to be.
When you call StopCoroutine(timeOutCoroutine);
you just tell Unity to remove the coroutine with this iterator object from any scheduled events, thus will terminate the coroutine. However your statemachine object of course is still in the same state. When you re-schedule your iterator object, Unity starts a new coroutine with your iterator object. Though it should be obvious that the statemachine will just continue where you left off when you stopped the coroutine.
Solution
If you want a coroutine to be restarted you would need to create a new instance of your statemachine object by calling your generator method again to get a fresh instance. As I mentioned the IEnumerator interface has a Reset method, however the compiler magic does not implement it at all as for most custom iterators there's no sensible way how the compiler could figure out what needs to be done in order to "reset" it.
Note that in some cases it might by useful to implement coroutines as an IEnumerable instead of IEnumerator. Though this only makes sense when your coroutine has parameters. The IEnumerable essentially can capture those parameters. So when you want to start a new instance of your coroutine you can just use the IEnumerable object and call GetEnumerator on it to get a new IEnumerator object. This might be useful in some OOP / abstract code that should be able to start arbitrary coroutines.
Thanks a lot for such an In deep an awesome explanation, I will have to take a second or third read to fully understand the internal functuonality. It is the most fully detail explanation I have found, I think it should get wikified. Thanks as always!
Answer by TheAwesomeLyfe · Jul 09, 2020 at 11:27 AM
@xxmariofer Try Adding a delay between Start Coroutine and Stop Coroutine. From what I see in your code, you are starting the Coroutine and after one second it's supposed to stop by calling stop. However since start Coroutine is called after it, this happens immediately giving you the unwanted result as the Coroutine never stopped.
IEnumerator timeOutCoroutine; public float m_Delay = 0.5f; IEnumerator Start() { timeOutCoroutine = TestRoutine(); StartCoroutine(timeOutCoroutine); yield return new WaitForSeconds(1f); StopCoroutine(timeOutCoroutine); //Add a delay here yield return new WaitForSeconds(m_Delay); StartCoroutine(timeOutCoroutine); yield return new WaitForSeconds(20f); StartCoroutine(timeOutCoroutine); }
If it seems like the delay isn't long enough and it starts to fast after stop, just increase the m_Delay value to something higher than 0.5.
Hope this helps.
---Update--- @xxmariofer
I tested your script and i saw where the coroutine will start but not stop after one second of your WaitForSeconds in the Start Method. So i removed the " timeOutCoroutine = TestRoutine();" and instead used TestRoutine() directly in the StartCoroutine() Function.
Example : StartCoroutine(TestRoutine());
The Delays i mentioned earlier is no longer needed as when i tested the new Solution it seems to work on my end. Here is the Code
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestScript : MonoBehaviour
{
IEnumerator timeOutCoroutine;
IEnumerator Start()
{
StartCoroutine(TestRoutine());
yield return new WaitForSeconds(1f);
StopCoroutine(TestRoutine());
StartCoroutine(TestRoutine());
yield return new WaitForSeconds(20f);
StartCoroutine(TestRoutine());
}
IEnumerator TestRoutine()
{
Debug.Log("Start");
float timer = 0;
while (timer < 10)
{
timer += Time.deltaTime;
yield return null;
}
Debug.Log("End");
}
}
Hello thanks for posting, i have updated the question with extra information
tested your suggestion without luck continues the same behaviour here is the updated code ->
IEnumerator timeOutCoroutine;
void Start()
{
timeOutCoroutine = TestRoutine();
StartCoroutine(Initialize());
}
IEnumerator Initialize()
{
StartCoroutine(timeOutCoroutine);
yield return new WaitForSeconds(1f);
StopCoroutine(timeOutCoroutine);
yield return new WaitForSeconds(1f);
StartCoroutine(timeOutCoroutine);
yield return new WaitForSeconds(20f);
StartCoroutine(timeOutCoroutine);
}
IEnumerator TestRoutine()
{
Debug.Log("Start");
float timer = 0;
while (timer < 10)
{
timer += Time.deltaTime;
yield return null;
}
Debug.Log("End");
}
@xxmariofer I will play around with this and find a better and more effective solution. I will update the answer shortly for you to try.
Ok thanks! A quick hack is to simply have a variable cancel and check every iteration if the variable is true, and if it is simply break the coroutine but if the coroutines get complex would add extra unneedee complexity, maybe it is a bug? It should simply work with the first code, shouldnt it?