- Home /
Brain exploding ... Problem with an internal Coroutine manager
IEnumerator's MoveNext() going crazy
Hello,
calling all C# superbrains, as I smell a deep and complex processing logic problem here :)
I'm trying to build an internal Coroutine manager, so that I can make sure of being able to use a home made version of StopCoroutine() without being restricted to one single argument.
Things go pretty well, except one big detail.
Architecture of the (small) manager
1) Uses an extension of IEnumerator (let's call it "MyCoroutine"), which just takes a reference into its constructor to the real IEnumerator function I want to use. It also adds a "stop" bool value, and overrides the MoveNext() C# function, inserting a Delete function when "stop" is set to true.
2) Each time I want to create a new instance of MyCoroutine, I put it into a Dictionary
3) Each time I call the MyCoroutine Delete() function, it just override its MoveNext() function for it to return False (which ends its Unity life cycle basically), and then deletes the instantiated MyCoroutine class into that Dictionary.
The extended class
Here's the class (inspired by : forum.unity3d.com/threads/102817-Coroutine-Control-and-StartCoroutine()-Coroutine) :
public class UniqueCoroutine : IEnumerator {
public bool stop;
public bool _moveNext;
string _name;
IEnumerator enumerator;
MonoBehaviour behaviour;
public readonly Coroutine coroutine;
public UniqueCoroutine(MonoBehaviour _behaviour, IEnumerator _enumerator, string _refName)
{
stop = false;
behaviour = _behaviour;
enumerator = _enumerator;
_name = _refName;
coroutine = this.behaviour.StartCoroutine(this);
}
public void DeleteCoroutine(){
//called from any other script, launch a Deletion process
// (can't delete it immediately, it would mess up the Unity process)
stop = true;
}
bool KillCoroutine(){
try{
return false;
} finally{
//Delete the Dictionary's entry, "finally" used to ensure
//that the reference is killed right after Unity kills the process
_model.KillCoroutine(behaviour, _name);
}
}
public object Current { get { return enumerator.Current; } }
public bool MoveNext() {
if (stop) {
return KillCoroutine();
} else {
_moveNext = enumerator.MoveNext();
if (stop) {
return KillCoroutine();
} else if (!_moveNext) {
DeleteCoroutine();
return false;
} else {
return true;
}
}
}
public void Reset() { enumerator.Reset(); }
And that's it. It works so far, and let me have full control over how many instances of one Coroutine are running, and moreover allows me to stop these at will, witout being limited to Unity's StopCoroutine() single parameter restriction.
But ... Something weird also happens...
The problem
If I decide to stop-delete one IEnumerator from a block of code which is nested inside that very IEnumerator, and if there was still some other code blocks remaining after that deletion point, the following happens :
IEnumerator properly stops and deletes itself
But the next time I launch a MyCoroutine from the same IEnumerator reference function, then as soon as a yield statement is happening, the MoveNext() litterally jumps toward the point where the last MyCoroutine instance was left before its deletion.
example :
IEnumerator Test(){
//step 1
(pseudocode : init variables)
//step 2
yield return new WaitForSeconds(1f);
//step 3
while (condition != true){
blablablabla
//step 4
yield return new WaitForEndOfFrame();
}
//step 5
(pseudocode : bleh bleh bleh bleh)
//step 6
if (condition == true)
DeleteThisCoroutine();
//step 7
(pseudocode : yadda yadda yadda yadda)
}
So, practically :
the first instance of this IEnumerator would go well. I decide to stop it in step 6, leaving the step 7 undone.
if I call the same IEnumerator via another MyCoroutine right after its deletion, the code would perform everything up to step 2, and then would jump right toward step 7. Yes, it would have skipped step 3, 4, 5 and 6 automagically.
Guesses and theories
Theory #1 : Even if I make sure of forcing MoveNext() to return False when I want to delete a MyCoroutine, so that Unity detects its end and deletes its process (right before deleting its very existing instance), the process still runs and therefore makes the next call for the exact same IEnumerator reference completely messed up.
Theory#2 : The MyCoroutine constructor IEnumerator parameter is not creating a new instance of it, but just pointing a reference. That would be the most plausible explanation imho.
Experimented solutions
tried to never delete a MyCoroutine instance manually, but letting the engine end its life cycle naturally by forcing MoveNext() to return false. Calling any new instance of the same IEnumerator reference would add a new stack in a List instead of just replacing it (and other operations of List cleaning when necessary). This should be the solution in theory, as no IEnumerator is deleted while it's still active in Unity. But the problem remains.
IEnumerator.Dispose() function would have been ideal, but it's not supported in .NET 2.0 ... So I tried to convert the constructor's IEnumerator reference to IEnumerator, which supports Dispose(), but Unity.StartCoroutine() doesn't support that type.
Conclusion
'm helpless right now. The only remaining solution would be to rebuild the manager around StopCoroutine restrictions, by creating single lined coroutines which would call a yield return StartCoroutine(anyIenumeratorReference). Calling a StopCoroutine would stop it properly at any time, but I'm not sure it would stop the running IEnumerator instantly. I will try that.
But, if anyone got an idea about that MoveNext() "bug" above, I'd really appreciate as it would save this architecture (which is better I guess, because giving more control).
Answer by n0mad · Oct 28, 2011 at 09:11 PM
I made a try... and finally :)
Ok, sorry for the pun :p I realized after turning the problem around several times that I was trying to achieve something extremely complicated from the start : to perform an "conditionally atomic" MoveNext().
Explanation : in the example I provided, I wanted my coroutine to stop parsing itself whenever I was destroying it. Which basically means : As long as coroutine exists, MoveNext() up to the next YieldInstruction, and if it is null, treat the deletion point as a yield itself without having to write a "yield".
That could have some use in some very complicated scenarii, when you want to stop a coroutine line-by-line processing immediately, without having to put a yield at each line. But this mindset is not correct from the start imho. That would mean "I'm too lazy to encapsulate my events so I want an automagic script stopper". That would also contradict with the whole "yield" philosophy (aka. creating precise breakpoints).
So in the end, I abandonned this idea and came back to forcing myself to use yield for coroutine breakpoints. Result is a very cleaner workaround that still allows me to Stop coroutines without being restricted to StopCoroutine's single argument. It also stops the coroutine at its next yield whenever its instance is deleted. You don't necessarly have to create a "yield break" anymore after deleting it from an external script, just have any yield (preserving the coroutine workflow, in fact).
It also ensures that Unity's coroutine triggered by UniqueCoroutine is not lost into dark processing hells when you're destroying the UniqueCoroutine instance (which was causing the MoveNext() uncontrollable behaviour in the first place). It will now last itself cleanly.
Solution
The centralized Start/Stop MyCoroutine functions
I still have my Model centralized class ("_model" in the examples above), and it still have its StartMyCoroutine / StopMyCoroutine functions :
public static void StartMyCoroutine(MonoBehaviour behaviour, IEnumerator enumerator, string _name){
StopUCoroutine(behaviour, _name);
_coroutines.Add(_name, new UniqueCoroutine(behaviour, enumerator, _name));
}
public static void StopMyCoroutine(MonoBehaviour behaviour, string _name){
_coroutines.Remove(_name);
}
Except now _coroutines is a simplier Dictionary, of type Dictionary<string, UniqueCoroutine>
.
The UniqueCoroutine final state
t is now much simplier, and doesn't need any "stop" boolean anymore. It doesn't even need the "_name" and "behaviour" reference, but I'll keep 'em "just in case of" one moment where I'd want more control over the class. But you can remove them.
The new thing is this little "_coroutineManager" inserted inside the constructor. I'll explain it in the chapter below.
Only thing to know is that it can't be called just with this pointer, as it is not static. It's up to the coder to pull whatever way he wants to call it. (I used a more complicated way, but it would have been messy to explain it here, so this mini-pseudocode is more fitting).
public class UniqueCoroutine : IEnumerator {
public string _name;
public IEnumerator enumerator;
public _baseClass behaviour;
public readonly Coroutine coroutine;
public UniqueCoroutine(MonoBehaviour _behaviour, IEnumerator _enumerator, string _refName)
{
behaviour = _behaviour;
enumerator = _enumerator;
_name = _refName;
coroutine = _coroutineManager.RunUniqueCoroutine(this);
}
public object Current { get { return enumerator.Current; } }
public bool MoveNext() {
return enumerator.MoveNext();
}
public void Reset() { enumerator.Reset(); }
}
The new _coroutineManager class
his is just one unique MonoBehaviour attached to any gameObject you want, which will serve as some headquarters for all the UniqueCoroutine you create.
Here is the _coroutineManager's function called by UniqueCoroutine constructor :
public Coroutine RunUniqueCoroutine(UniqueCoroutine uniqueCoroutine){
return StartCoroutine(_uniqueCoroutine(uniqueCoroutine));
}
public IEnumerator _uniqueCoroutine(UniqueCoroutine uniqueCoroutine){
while(uniqueCoroutine != null && uniqueCoroutine.enumerator.MoveNext()){
yield return uniqueCoroutine.enumerator.Current;
}
}
Simple as that. As long as the UniqueCoroutine instance is existing, Unity is parsing it. As soon as it is destroyed, Unity registers it as an ended coroutine.
The enumerator.Current returns whatever your "yield" statements do inside the original IEnumerator function, and therefore _uniqueCoroutine function will wait for it before performing the next MoveNext().
Thanks Bunny83 for your help, you contributed quite effectively in making me understand how simple could it be in the end.
I hope this mini framework could help some people around. Cheers, see ya.
Additionally, you could even put the whole UniqueCoroutine class to trashcan, and call the _coroutine$$anonymous$$anager.RunUniqueCoroutine() directly from the $$anonymous$$odel. These model functions would be something like this :
public static void Start$$anonymous$$yCoroutine($$anonymous$$onoBehaviour behaviour, IEnumerator enumerator, string _name){
StopUCoroutine(_name);
_coroutines.Add(_name, enumerator);
_coroutine$$anonymous$$anager.RunUniqueCoroutine(_coroutines[_name]);
}
public static void Stop$$anonymous$$yCoroutine(string _name){
_coroutines.Remove(_name);
}
And the _coroutine$$anonymous$$anager._uniqueCoroutine function would look like this now :
public IEnumerator _uniqueCoroutine(IEnumerator enumerator){
while(enumerator != null && enumerator.$$anonymous$$oveNext()){
yield return enumerator.Current;
}
}
I didn't test it, but it would theorically work. Although I personally prefer the UniqueCoroutine class approach, as it gives more control.
Answer by Bunny83 · Oct 28, 2011 at 02:03 PM
You left out the most important thing: how do you create an instance of your coroutine? You know that when calling a generator function (a function with a yield statement) it returns an object instance which represents your coroutine as an IEnumerator. It doesn't work like a delegate or function pointer. You have to create a NEW instance of your wrapper class as well as getting a new IEnumerator instance of your actual generator / coroutine to start a new one.
The true magic happens inside the class that is returned by your generator function. In this class .NET / Mono stores the actual state of the coroutine. If you want to start a new one you have to call your coroutine again to create a new instance. If you work with the old enumerator object you will of course just continue the enumeration where you have stopped the last time.
To start a new one you have to do something like:
IEnumerator myCoroutine() // That's our generator function / coroutine
{
yield return new WaitForSeconds(5.0f);
}
// Now start a new instance:
new UniqueCoroutine(myScript, myCoroutine(), "bla");
// |
// |
// This call to your generator will return a new IEnumerator object
Hopefully Unity will include a public Stop() function in their Coroutine-class. That would be the easiest way to stop it from outside. StartCoroutine returns an instance of Unitys internal used class Coroutine
but all you can do with it at the moment is to use it inside yield statement.
ps. I'm a bit confused about where your dictionary is located and where that _model
variable comes from... How do you actually stop a coroutine? The overall concept of your little "hack" is well planned (at least from what i can see) and it actually should work, so it would help to see how you use it ;)
Thanks Bunny83 for your clarification ! I'll try your suggestion right now and keep you informed :)
Also, sorry for the "_model" reference, it is a remain of my actual code, never was it intended to be there :p
The Dictionary is located into that "_model" precisely, which is a Singleton (or whatever it's called, it's a static members/functions only class). I use it to call several $$anonymous$$odel type functions without having to use billions of object pointers.
How do I stop a coroutine right now :
1) I call a "Stop$$anonymous$$yCoroutine(myCoroutineEntryName)" function inside "_model" class. myCoroutineEntryName being the name I gave it when added inside the Dictionary.
2) this function just turns the myCoroutine instance's "stop" boolean to true.
3) in this very myCoroutine instance, $$anonymous$$oveNext() detects that "stop" is True, so bypasses the referenced ienumerator.$$anonymous$$oveNext() and returns the result of function myCoroutine.DeleteCoroutine().
4) DeleteCoroutine() uses a try...finally block, in which "try" just returns False (to force a stop), and "finally" performs a deletion of the myCoroutine entry into "_model" Dictionary.
This is used to ensure that never a myCoroutine Dictionary entry is deleted before the coroutine is naturally ended by Unity.
This is a great article to understand how a coroutine scheduler works. Inside this post there's a small link to this blog which explains roughly how a generator function works.
It really helps to take a look at the generated code with a reflector like ILSpy. Each coroutine will be turned into a function (which creates an instance of the IEnumerator class) and an internal class which implements an IEnumerator (This class contains the actual code from your coroutine).
These internal classes can't be decompiled into C# because the C# compiler uses some "hacks" (mostly direct jumps) which are not supported by C# but you can view the class in IL (Intermediate_Language)
Thanks for the link, I'll study that. Also, I realize I was already declaring new myCoroutine instances like the way you suggested. Here is a typical declaration :
Start$$anonymous$$yCoroutine(this, anyIEnumerator(), "myCoroutineName");
"this" being the $$anonymous$$onoBehaviour where I want to perform the myCoroutine.StartCoroutine().
Here a "small" example what happens behind the scenes when you create a simple coroutine:
IEnumerator myCoroutine(float delay)
{
yield return new WaitForSeconds(delay);
string tmp = "HelloWorld";
yield return "Bla";
Debug.Log(tmp);
}
This coroutine is turned into something like this:
IEnumerator myCoroutine(float delay)
{
return new Internal_myCoroutine(delay);
}
class Internal_myCoroutine
{
int currentState = 0;
object current;
float parameterDelay;
string localTmp;
public Internal_myCoroutine(float delay)
{
parameterDelay = delay;
}
public bool $$anonymous$$oveNext()
{
switch(currentState)
{
case 0:
{
current = new WaitForSeconds(parameterDelay);
currentState++;
return true;
}
case 1:
{
localTmp = "HelloWorld";
current = "Bla";
currentState++;
return true;
}
case 2:
{
Debug.Log(localTmp);
currentState++;
return true;
}
case 3:
{
return false;
}
}
}
public object Current
{
get
{
return current;
}
}
}
Yup, seems familiar enough :) This is why I'm completely lost at the $$anonymous$$oveNext() stuff I'm facing : Here this would translate into the fact that the "new Internal_myCoroutine(delay);" portion would not reinitialize its "currentState" :/
I guess that's what IEnumerator.Reset() is used for, but it's not accessable by code (engine returns a "NotSupportedException").
Answer by n0mad · Oct 28, 2011 at 05:46 PM
Oh, I may not having expressed myself very well, but I do create a new instance by calling the generator function, and also do create a new instance of my wrapper class :)
Sorry I should have written the whole creation process. Here it is :
1) I'm calling _model's creation function :
_model.StartMyCoroutine(monoBehaviourWhereTheGeneratorResides, myCoroutine(), "myCoroutineName");
2) inside _model class, the StartMyCoroutine() function looks like this :
public static void StartMyCoroutine(MonoBehaviour behaviour, IEnumerator enumerator, string _name){
StopUCoroutine(behaviour, _name);
if (!myDictionary.ContainsKey(_name)){
_coroutines.Add(_name, new List<UniqueCoroutine>());
}
_coroutines[_name].Add(new UniqueCoroutine(behaviour, enumerator, _name));
}
public static void StopUCoroutine(MonoBehaviour behaviour, string _name){
//orders the oldest instance of UniqueCoroutine named "_named" to stop
//DeleteCoroutine() just force MoveNext() to return False
if (_coroutines.ContainsKey(_name) && _coroutines[_name].Count > 0){
_coroutines[_name][0].DeleteCoroutine();
}
}
Where myDictionary is of type Dictionary<string, List<UniqueCoroutine>>
.
Each key is the name of one generator I want to start, and each entry's List is a chronological succession of instances of this generator.
This list is used so that each time I call StartMyCoroutine(), if an instance of the generator it's asking for is already running, the function doesn't have to wait for its coroutine lifecycle to stop in order to trigger a new version of it (while still ensuring that the old version will stop as soon as its MoveNext() is called).
Your answer
Follow this Question
Related Questions
how to restart coroutine 2 Answers
Item duration doesn't stack 3 Answers
Can't Stop couroutines 1 Answer
Coroutines IEnumerator not working as expected 2 Answers
How to force Coroutine to finish 1 Answer