Loops using coroutines - c#

I have a question about coroutine behaviour in case of loops, see following code extract as example done on a Unity C# script:
void Start() {
StartCoroutine(FSM());
}
IEnumerator FSM() {
state="State1";
while (true) {
Debug.Log("State "+state);
yield return StartCoroutine(state);
}
}
IEnumerator State1() {
while (true) {
if (stateTransitionCond) {
state = "NextState";
yield break;
}
yield return null;
}
}
The status machine works fine, but while the current status is Status1 (stateTransitionCond==false), due to the yield return null inside the loop of State1() routine, I was expecting that loop inside FMS() also performs another iteration generating debug log 'Debug.Log("State "+state);'.
In other words I was expecting a lot of debug log (one for each iteration of State1() routine, when status is Status1) but in the reality only 1 execution is performed while status is Status1.
So I suppose I miss something about yield functionality, is there anybody that can explain me this behaviour?

Your issue stems from the fact that your code does not break out of the State1() method until stateTransitionCond == true.
The method starting the coroutine, FSM(), is not returned to until State1 is finished. In other words, control flow does not return to the calling method until the coroutine is complete. I believe this is due to the fact that you are yield-ing State1 inside FSM (yielding another coroutine). Obviously, "normal" methods do not wait for the coroutine to finish before continuing execution.
Please see the code sample below for an illustrative example:
using UnityEngine;
using System.Collections;
public class CoroutineTest : MonoBehaviour {
// current FSM state
public string state = "";
void Start()
{
StartCoroutine(FSM());
}
IEnumerator FSM()
{
state = "State1";
while (true)
{
Debug.Log("State: " + state);
// ExecuteOnce will execute exactly once before returning to the outer function
yield return StartCoroutine(ExecuteOnce());
// ExecuteIndefinitely will execute indefinitely until its while() loop is broken
// uncomment to test
//yield return StartCoroutine(ExecuteIndefinitely());
}
}
IEnumerator ExecuteOnce()
{
Debug.Log("Calling ExecuteOnce()");
yield return new WaitForSeconds(1f);
}
IEnumerator ExecuteIndefinitely()
{
Debug.Log("Calling ExecuteIndefinitely()");
while (true)
{
Debug.Log("Inside ExecuteIndefinitely()");
yield return new WaitForSeconds(1f);
}
}
}

Related

Using a Coroutine in a loop

I have a question about coroutines in a loop. More specific about how I can achieve it that my loop continues to check the condition until the waitForSeconds in the coroutine are over.
I have attached a screenshot of my code. My problem is that the Line "Now I am executed" is shown right after myAudio.Play();
It makes sense since the coroutine only waits for the statements after the yield return but how can I make it work with the for loop?
Any help would be appreciated! Thank you so much.
StartCoroutine starts each IEnumeratoras a new Coroutine and immediately continuous with the rest of the code.
So as said without having further context of your code what you would do is something like
private void playDBStack()
{
fillList();
StartCoroutine(playDBStackRoutine())
}
private IEnumerator playDBStackRoutine()
{
for(var i = 0; i < 2; i++)
{
// executes playAudio and waits for it to finish
yield return playAudio();
}
}
private IEnumerator playAudio()
{
var x = chooseList(score);
// executes PlayAndWait and wait for it to finish
yield return PlayAndWait(x);
}
this way you execute and at the same time wait until the IEnumerators are finished before going to the next one.
Some further notes:
The line
myAudio.GetComponent<AudioSource>();
does absolutely nothing.
The entire method updateScore in its current state could simply be replaced by using
score++;
I would consider a method that can be replaced by a simple operator "code smell" ;)

How can I prevent my code from yielding every frame?

I am calling a method that call itself to crawl through terrain and create zones. However when the zones become to large the process ends in a stack overflow. By forcing the code to yield and take its time it finishes to completion successfuly and crawls the 3 zones in my map. However the method I am using is yielding EVERY single frame and I don't know how to make it yield just every 100 frames, causing it to be extremely slow to finish. Here is the pseudo code of what I am doing for readability:
public int timer = 0;
void Awake(){
StartCoroutine(crA);
}
public IEnumerator crA(){
//stuff
yield return StartCoroutine(crB());
//stuff that needs to happen only after crB finishes
}
public IEnumerator crB(){
timer = 0;
yield return StartCoroutine(crC());
}
public IEnumerator crC(){
//Crawiling code, crawls terrain to create a zone
if(x){ yield break;}
timer++;
//vv I WANTED IT TO YIELD ONLY IN HERE
if (timer ==100){
timer = 0;
yield return null;
}
//^^
yield return StartCoroutine(crC());
yield return StartCoroutine(crC());
yield return StartCoroutine(crC());
yield return StartCoroutine(crC());
yield return StartCoroutine(crC());
}
it seems yield return startcoroutine is causing a yield but I don't know what to use instead. Any help would be appreciated.
As said in general as long as you use yield it will at least yield for one frame like
yield return null;
does.
What you could do instead is use the IEnumerator.MoveNext "manually" without yielding it. This is basically what the Unity coroutine is calling once per frame if you used StartCoroutine.
And then only yield when you want to do so. Given your pseudo code something like e.g.
private void Awake()
{
StartCoroutine(crA);
}
public IEnumerator crA()
{
//stuff
yield return crB();
//stuff that needs to happen only after crB finishes
}
public IEnumerator crB()
{
timer = 0;
// "Manually" run the routine instead of using Unity Coroutine interface
var crc = crC();
while(crc.MoveNext())
{
if(timer >= 100)
{
yield return null;
timer = 0;
}
}
}
public IEnumerator crC()
{
//Crawiling code, crawls terrain to create a zone
if(x) yield break;
timer++;
yield return crC();
yield return crC();
yield return crC();
yield return crC();
yield return crC();
}
And then as said you could do it time based instead of using a frame/call counter rather using e.g. a StopWatch
private const float TARGET_FRAMERATE = 60;
public IEnumerator crB()
{
var targetMilliseconds = 1000f / TARGET_FRAMERATE;
var sw = new StopWatch();
sw.Start();
// "Manually" run the routine instead of using Unity Coroutine interface
var crc = crC();
while(crc.MoveNext())
{
if(sw.ElapsedMilliseconds >= targetMilliseconds)
{
yield return null;
sw.Restart();
}
}
}
So whenever the last execution exceeded the target frame-rate it will yield one frame. You'll have to play a bit with the value .. probably something like 30 or even 24 might already be enough depending on your usecase.
It basically is a tradeoff between frame-rate and actual realtime duration.
Note: Typed on smartphone but I hope the idea gets clear

How to clear all user keystrokes in unity c#?

i'm writing some c# code within Unity game i'm developing.
and there's a problem i can't fix.
i'm using StartCoroutine in my code and inside i'm calling another StartCoroutine. i know that when doing that, there's two threads executing the code in those parts.
but not if i'm calling yield return to that StartCorotuine;
IEnumerator StartLoop()
{
yield return StartCorotuine(GetInputFromUser()); // 1
// some variables
yield return StartCorotuine(GetInputFromUser()); // 2
}
IEnumerator GetInputFromUser()
{
if (Input.GetKeyDown(KeyCode.Space))
{
print("IN");
}
else
{
yield return null;
}
}
the problem is, it prints twice.
the buffer isn't empty from the first time calling "GetInputFromUser()" and it keeps it until the second time and enters immediately to the if even when i'm not pressing Space.
things i've tried:
Console.Clear();
Console.ReadKey();
Console.ReadLine();
UnityEngine.Rendering.CommandBuffer.Clear();
while(Console.KeyAvailable)
Console.ReadKey();
and some more i can't even remember.
either the whole idea is wrong and somehow it worked till now. or i'm missing something.
any advise? i've googled everything i could think of.
[[[SOLVED]]]
I don't know how it works here, and if you're closing the post or something. but I've solved it.
The problem was, as someone suggested, that the calls happened in the same frame, so i've added another null in between to force skipping to the next frame:
IEnumerator StartLoop()
{
yield return StartCorotuine(GetInputFromUser()); // 1
// some variables
yield return null; // skipping frame
yield return StartCorotuine(GetInputFromUser()); // 2
}
IEnumerator GetInputFromUser()
{
if (Input.GetKeyDown(KeyCode.Space))
{
print("IN");
}
else
{
yield return null;
}
}
Thanks for the helpers!
First of all: Coroutines have nothing to do with threads! Every Coroutine is executed in the Unity main thread and get their MoveNext executed right after Update has finished for that behaviour.
Your problem should be that in
IEnumerator GetInputFromUser()
{
if (Input.GetKeyDown(KeyCode.Space))
{
print("IN");
}
else
{
yield return null;
}
}
in the case there was no input you do yield return null; which causes the Coroutine to wait at least one frame!
So the second one is executed in the next frame.
But in case there was an input you do not wait so the next Coroutine is directly started where again you don't wait!
Result: You get your print twice without waiting at all.
It's not fully clear what you are trying here but I assume you want to wait until Space is pressed so you should rather use e.g.
bool GetInputFromUser()
{
if (Input.GetKeyDown(KeyCode.Space))
{
print("IN");
return true
}
return false;
}
And use it like
// This now waits until the user presses Space
yield return new WaitUntil (GetInputFromUser);
or simply also yield for one frame in case there was the input like
IEnumerator GetInputFromUser()
{
if (Input.GetKeyDown(KeyCode.Space))
{
print("IN");
}
yield return null;
}
though
the goal is to check if the user presses the SpaceBar more than once.
It is very very unlikely that a user manages to press the key down twice in two consecutive frames.
StartLoop starts inside Start()
but at this moment the user can't provide any input yet anyway. So you check only once if the user presses a key within the first frames..
In Start()
I'm waiting for the first Space stroke.
once he has, I'm starting the StartCorotuine(StartLoop()); sorry for the mixup.
I'm using it like that, maybe it'll be more clear, once the user Starting the loop, I'm waiting for him to press Space. once he has a timer is set on and once the timer has timed the IEnumerator return without Input from the User and I'm getting to the next iteration, the only thing I want to do is to check if the user pressed twice on that Space.
{
while (timer < timeSetInStart)
{
If(Input.GetKeyDown(KeyCode.Space))
{
print("IN";
yield break;
}
else
{
yield return null;
}
}
}
I'm not sure if there's a specific reason why this needs to be in a coroutine but it might be easier to handle this all in Update():
bool spacePressed = false;
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
if (spacePressed)
{
print("Space pressed twice");
spacePressed = false;
}
else
{
print("Space pressed once");
spacePressed = true;
}
}
}

Why isn't my "yield" working?

i'm very new to programming and i have a feeling that there is a very stupid mistake here. But can anyone explain me, why instead of 4 messages with a delay of 2 seconds between, i instantaniously get the last message shown only.
using UnityEngine;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using UnityEngine.UI;
public class Wait : MonoBehaviour {
private int i = 0;
public string[] message;
[SerializeField]
private Text toText;
public IEnumerator Message(float waitTime)
{
toText.text = message[i];
i++;
yield return new WaitForSeconds(waitTime = 2f);
}
void Start()
{
StartCoroutine(Message(i));
StartCoroutine(Message(i));
StartCoroutine(Message(i));
StartCoroutine(Message(i));
}
}
void Start()
{
StartCoroutine(Message(i));
StartCoroutine(Message(i));
StartCoroutine(Message(i));
StartCoroutine(Message(i));
}
I don't think that is doing what you think it should. This will not wait for each StartCoroutine to finish and will call the next StartCoroutine.
This is what happens:
The first StartCoroutine(Message(i)); call will start the Message function.
Once it meets the yield return new WaitForSeconds(waitTime = 2f); line of code, it will then jump back into the Start() function.
The next StartCoroutine(Message(i)); will be called then the-same thing will happen again.
When calling a coroutine function from a non coroutine function, as long as you have yield return new WaitForSeconds, yield return null;, or yield return what-ever YieldInstruction is implemented, the execution will return to that non coroutine function in-which the StartCoroutine function was called from and continue to execute other code.
To make coroutine wait for another one to finish, make the StartCoroutine(Message(i)); function call from another coroutine function. This will allow you to yield each coroutine function call. This is called chaining coroutine.
To chain or yield a coroutine function call, simply put yield return in front of the StartCoroutine function. yield return StartCoroutine(Message(i));
public class Wait : MonoBehaviour {
private int i = 0;
public string[] message;
[SerializeField]
private Text toText;
public IEnumerator Message(float waitTime)
{
// toText.text = message[i];
i++;
yield return new WaitForSeconds(waitTime = 2f);
}
void Start()
{
StartCoroutine(startMessage());
}
IEnumerator startMessage()
{
yield return StartCoroutine(Message(i));//Wait until this coroutine function retuns
yield return StartCoroutine(Message(i));//Wait until this coroutine function retuns
yield return StartCoroutine(Message(i));//Wait until this coroutine function retuns
yield return StartCoroutine(Message(i));//Wait until this coroutine function retuns
}
}
Now, each StartCoroutine(Message(i)); call will wait until the first one finishes. You can always use a boolean variable to do this but it is much better to yield the StartCoroutine call.
The reason the text is set instantly is because StartCoroutine will execute the enumerator from Message.
The first two things that happen is setting the text and incrementing i. Only after that is done, you will yield the WaitForSeconds. It is at that point StartCoroutine will pause further execution of Message.
If you had had a line after the yield return you would have seen the effects of that after the 2 seconds.
in the example in the docs you can also see the behavior after the yield return Wait
https://docs.unity3d.com/ScriptReference/MonoBehaviour.StartCoroutine.html
I would suggest running the next test to become more familiar with how yield return works:
IEnumerator MessageOuter() {
Console.WriteLine("outer 1");
var inner = MessageInner();
Console.WriteLine("outer 2");
return inner;
}
IEnumerator MessageInner() {
Console.WriteLine("inner 1");
yield return new WaitForSeconds(1);
Console.WriteLine("inner 2");
yield return new WaitForSeconds(1);
Console.WriteLine("inner 3");
}
void Start() {
Console.WriteLine("start 1");
var outer = MessageOuter();
Console.WriteLine("start 2");
StartCoroutine(outer);
Console.WriteLine("start 3");
}

How to stop co-routine?

When two co-routines are running, how do you stop the first co-routine?
GLOBALS.stableTime = 5;
IEnumerator StableWaittingTime ()
{
yield return new WaitForSeconds (1f);
if (GLOBALS.stableTime == 0) {
GameManager.instance.LevelFaildMethod ();
} else {
GameManager.instance.stableWaittingTime.text = GLOBALS.stableTime.ToString ();
GLOBALS.stableTime--;
StartCoroutine ("StableWaittingTime");
}
}
There are three ways to stop coroutines.
The first is to call StopAllCoroutines(), which will obviously stop all running coroutines.
The second is to call StopCoroutine(coroutine), where coroutine is a variable name given to your IEnumerator.
And the third is to do a yield break from within the coroutine.
Worth noting is that both StopAllCoroutines and StopCoroutine can only stop a coroutine when the coroutine reaches a yield return *.
So if you have two coroutines with the same name and you want to stop the one you are executing in you do yield break.
Interestingly, if you want to stop every other coroutine besides the one you are executing in, you call StopCoroutines() from within that coroutine.
#Imapler answer is almost all you need. I would just add that StopCoroutine method of MonoBehaviour is overloaded and has 3 types of parameters, so it is possible to stop many coroutines of same name.
For your need here, just use yield break; like this:
void Start ()
{
StartCoroutine (StableWaittingTime ());
}
IEnumerator StableWaittingTime ()
{
yield return new WaitForSeconds (1f);
if (false)
{
// do something
}
else
{
// do something
StartCoroutine (StableWaittingTime ());
yield break;
}
}

Categories

Resources