- Home /
Misunderstanding Co-routines?
Hello, I have a feeling that this problem is caused by some aspect of co-routines that I don't know about, but I'm not sure. I have a block of code (below) that is designed to rotate the player to peek around corners. It seems to be repeating in a really weird way though. Init (a debug.log in the code block) is hit several times when it should only be activating once. The counter runs for a few ticks, then it apparently decides that canPeek is false and restarts, only hitting Mark 1 (commented line in code block) a few times. When it does finally make it to Mark 2, it appeared to hit Mark 2, Mark 1, Mark 2, then Mark 4 a few times before deciding that adhering to while loops is for chumps and hitting a repeating pattern of Mark 1, Mark 2, Mark 5. I have no idea why it is doing this and it is mind boggling because of the conditions that must be activating and deactivating for this to happen. If anyone sees my issue I would love some input. Thank you.
(Note about code: A lot of it is just repeating for the different directions that rotDir represents; all those else ifs at the bottom are repeated with minimal changes. Also, the canPeek variable is set to true if the players movement would take them past the edge of a wall.)
IEnumerator PeekCover(int rotDir)
{
int count = 0;
float rotation = Mathf.Round(swapper.transform.parent.transform.eulerAngles.y);
bool inCorner = true;
Debug.Log(rotDir);
//check if against a wall
if(rotDir == 1)
{
if(rotation == 270)
inCorner = false;
} else if(rotDir == 2)
{
if(rotation == 90)
inCorner = false;
} else if(rotDir == 3)
{
if(rotation == 180)
inCorner = false;
} else if(rotDir == 4)
{
if(rotation == 0)
inCorner = false;
}
//end wall check
if(inCorner == false)
{
if(rotDir == 1)
{
Debug.Log("Init");
while(canPeek == true && count <= 20)
{
count++;
yield return null;
//Mark 1
}
//Mark 2
if(count >= 20)
transform.eulerAngles = new Vector3(-30f, 0f, 0f);
while(canPeek == true && count >= 19)
{
if(Input.GetKeyDown("left ctrl"))
{
canPeek = false;
inCover = 0;
rb.constraints = RigidbodyConstraints.None;
transform.eulerAngles.Set(0f, transform.rotation.y, 0f);
FindCover(swapper);
//Mark 3
}
yield return null;
//Mark 4
}
transform.eulerAngles.Set(0f, transform.rotation.y, 0f);
//Mark 5
}
else if(rotDir == 2)
{
while(canPeek == true && count <= 30)
{
count++;
yield return null;
}
if(count >= 30)
transform.eulerAngles = new Vector3(30f, 0f, 0f);
while(canPeek == true && count >= 30)
{
if(Input.GetKeyDown("left ctrl"))
{
canPeek = false;
inCover = 0;
rb.constraints = RigidbodyConstraints.None;
transform.eulerAngles.Set(0f, transform.position.y, 0f);
FindCover(swapper);
}
yield return null;
}
transform.eulerAngles.Set(0f, transform.rotation.y, 0f);
}
else if(rotDir == 3)
{
while(canPeek == true && count <= 30)
{
count++;
yield return null;
}
if(count >= 30)
transform.eulerAngles = new Vector3(0f, 0f, -30f);
while(canPeek == true && count >= 30)
{
if(Input.GetKeyDown("left ctrl"))
{
canPeek = false;
inCover = 0;
rb.constraints = RigidbodyConstraints.None;
transform.eulerAngles.Set(0f, transform.position.y, 0f);
FindCover(swapper);
}
yield return null;
}
transform.eulerAngles.Set(0f, transform.position.y, 0f);
}
else if(rotDir == 4)
{
while(canPeek == true && count <= 30)
{
count++;
yield return null;
}
if(count >= 30)
transform.eulerAngles = new Vector3(0f, 0f, 30f);
while(canPeek == true && count >= 30)
{
if(Input.GetKeyDown("left ctrl"))
{
canPeek = false;
inCover = 0;
rb.constraints = RigidbodyConstraints.None;
transform.eulerAngles.Set(0f, transform.position.y, 0f);
FindCover(swapper);
}
yield return null;
}
transform.eulerAngles.Set(0f, transform.position.y, 0f);
}
}
}
Answer by Bunny83 · Jan 07, 2016 at 04:23 AM
Well, the most important thing related to your problem is where and how do you start the coroutine. However that part is missing in your question.
If "Init" prints several times you most likely are starting the coroutine several times. A coroutine is actually an object which implements a statemachine and not just a method. The yield keyword + IEnumerator is pure "compiler magic".
Just for reference, this is how a usual IEnumerator / iterator might look like:
// not a coroutine !!!
IEnumerator Foo()
{
yield return "Hello";
yield return "World";
yield return "!!!"
}
Here we have a method that uses the yield keyword and returns an object that implements the IEnumerator interface. The object that is returned by that method is an instance of the statemachine class that the compiler generated for your "method".
The IEnumerator interface represents an object that can be "iterated". For that it provides us a "MoveNext" method which will advance the iterator to the next element. It also has a "Current" property which allows us to read / access the current "item".
Here's an example how you can iterate such an object:
IEnumerator iterator = Foo();
while (iterator.MoveNext())
{
Debug.Log((string)iterator.Current);
}
This will print 3 lines in the console:
Hello
World
!!!
As you can see, each time we call MoveNext the statemachine will advance to the next "yield return". Current contains whatever you "yield returned" inside the iterator method.
So how does Unity actually use this functionallity to implement coroutines?
IEnumerator SomeCoroutine()
{
Debug.Log("Start");
yield return new WaitForSeconds(1);
Debug.Log("End")
}
StartCoroutine(SomeCoroutine());
The method SomeCoroutine is again just an iterator method like we discussed above. That's nothing Unity invented. The key part is "StartCoroutine". What you do when you start a coroutine is:
You invoke your generator method which returns a new state machine instance for your coroutine.
You pass that object / instance to the StartCoroutine method
Unity will store that instance internally in it's coroutine scheduler. That scheduler will now start "iterating" your object by calling MoveNext. As a result the code in your coroutine will execute until the first yield. We "yield" a WaitForSeconds object. After MoveNext returns to the scheduler, the scheduler now examines the Current property to decide what comes next. It recognises the wait for seconds object and simply stores your object in some kind of list.
Some frames later the wait time has expired and the scheduler takes our iterator object from the shelf and again calls MoveNext. This will execute the next "part" of our original code until we reach the next yield or the end of the method. Once the end is reached MoveNext will return "false" which indicates that the iterator is finished. Unity will now simply kick the object out of its internal list and the coroutine is finished.
If you have a while loop inside your coroutine that while loop actually isn't a while loop ^^. Keep in mind the compiler creates a statemachine out of your code.
A very simple example:
IEnumerator FooBar()
{
while (true)
{
Debug.Log("Another frame");
yield return null;
}
}
This would roughly be translated to:
private class _FooBarIterator : IEnumerator
{
private object current;
public object Current {get { return current;}}
public bool MoveNext()
{
Debug.Log("Another frame");
current = null;
return true;
}
}
IEnumerator FooBar()
{
return new _FooBarIterator();
}
This would be an endless running coroutine. There's no real statemachine required in this case since each time we call MoveNext it will always return true since there is no end.
Another example
IEnumerator FooBar(int n)
{
for (int i = 0; i < n; i++)
{
Debug.Log("Frame: " + i);
yield return null;
}
}
This would translate to something like:
private class _FooBarIterator : IEnumerator
{
private object current;
private int state = 0;
public int n;
private int i;
public object Current {get { return current;}}
public bool MoveNext()
{
if (state == 0)
{
i = 0;
state = 1;
}
if (state == 1)
{
if (i < n)
{
Debug.Log("Frame: " + i);
current = null;
i++;
return true;
}
else
{
state = 2;
}
}
if (state == 2)
{
return false;
}
}
}
IEnumerator FooBar(int n)
{
var tmp = new _FooBarIterator();
tmp.n = n;
return tmp;
}
This is actually not correct since the increment of "i" would happen before the next iteration since the yield is in between. The compiler usually uses some nasty "goto" statements to jump around in the statemachine. However this is roughly what is generated by the compiler.
As you can see your local variables and method parameters (which are like "special" local variables) are actually converted into member variables of that statemachine class. It's the only way how the internal state of your code can be preserved.
The compiler is able to completely "unroll" your entire code in the iterator method into such a statemachine.
That is one heck of an explanation. Thanks. It definitely helped to fix the weird problem with hitting marks. I'm still having the problem of the final rotation not activating but I'll throw up another question for that because it's a whole different can of worms I think.
Your answer
Follow this Question
Related Questions
Flip over an object (smooth transition) 3 Answers
Start Coroutine after other has finished 4 Answers
Random smooth rotation on z-axis every other second 2 Answers
Multiple Cars not working 1 Answer