- Home /
How to ''stack'' coroutines, and call each one till all are executed?
My coroutine Lerpfunction() lerps an image to a larger size. This lerp takes 10 seconds. If the Lerpfunction() is called again during that 10 seconds the coroutines override each other, and I get weird behavior. Is it possible to, sort of ''stack'' these calls and call each Lerpfunction() after another, till all calls are executed?
I tried to solve it with a whileloop, Booleans and a Queue but without success.
Any suggestions?
IEnumerator Lerpfunction ()
{
//Lerp implementation
}
If I understand the question, which is debatable, then one approach would be to have a count variable that keeps track of how many instances of the coroutine are running.
Something like this...
int coroutineCount;
IEnumerator Resize(Texture2D t)
{
coroutineCount++;
// do stuff to resize t
coroutineCount--;
if (coroutineCount == 0) CarryOn();
}
void ResizeTexturesParallel(Texture2D[] ts)
{
coroutineCount = 0;
for (int i = 0; i < ts.Length; i++)
{
StartCoroutine( Resize(ts[i]));
}
}
void CarryOn()
{
// do whatever needs doing once all the textures have been resized
}
Whereas, if you need to call them one after the other (not clear why the resizing would need to use anything but local variables but...) just do it in a wrapping coroutine...
IEnumerator ResizeTexturesSerial(Texture2D[] ts)
{
for (int i = 0; i < ts.Length; i++)
{
yield return StartCoroutine( Resize(ts[i]));
}
CarryOn();
}
Thank you for your reply! The sprite that I needed to change was only a single one, and resize it multiple times. Nevertheless that was a good suggestion aswell!
Answer by Bunny83 · Jan 17, 2019 at 11:18 AM
You can stack them, but you have to do it manually. There are several ways this could be done. One is to use a Queue<IEnumerator>
and a boolean variable to determine if a coroutine is already running and then either just start a coroutine when none is running at the moment or instead of passing it to StartCoroutine you can store it in the queue. At the end of the coroutine you can check the queue and if it has items, just pop one and start that coroutine. While the overall setup is quite simple, it has some drawbacks. First of all if you want to queue several different coroutines, the starting of the queue coroutines need to be at the end of every coroutine you want to stack. Also starting / queue a coroutine always requires this if-else statement.
Another solution is to use a Queue in combination of a coordinator / director coroutine that is running all the time and that will actually start the coroutines at the right time(s).
Both approaches have one issue: You can not use StopCoroutine to terminate a running coroutine. You could clear the queue which would cancel all queued coroutines but the current one has to finish. If you need to be able to stop the coroutine at any time the first approach would allow this with a few modifications. If not the second one is the simplest one to use.
// Second case:
private Queue<IEnumerator> coroutineQueue = new Queue<IEnumerator> ();
void Start()
{
StartCoroutine(CoroutineCoordinator());
}
IEnumerator CoroutineCoordinator()
{
while (true)
{
while (coroutineQueue.Count >0)
yield return StartCoroutine(coroutineQueue.Dequeue());
yield return null;
}
}
IEnumerator Lerpfunction ()
{
//Lerp implementation
}
With this setup you can simply use
coroutineQueue.Enqueue(LerpFunction());
instead of
StartCoroutine(LerpFunction());
The nice thing is you can queue any kind of coroutine (as long as it is not stopped manually) without any special addition to the coroutine. The "CoroutineCoordinator" will pick up any coroutine that is queued and just start it if he is currently idling. If another coroutine is queued while the coordinator is already running a coroutine, nothing happens. When the first coroutine has finished it automatically starts the next one.
The first case i've mentioned would look like this:
// first case
IEnumerator runningCoroutine = null;
private Queue<IEnumerator> coroutineQueue = new Queue<IEnumerator> ();
IEnumerator Lerpfunction()
{
//Lerp implementation
runningCoroutine = null;
if (coroutineQueue.Count > 0)
{
runningCoroutine = coroutineQueue.Dequeue();
StartCoroutine(runningCoroutine);
}
}
To start / queue a coroutine you would do this:
if (runningCoroutine == null)
{
runningCoroutine = LerpFunction();
StartCoroutine(runningCoroutine);
}
else
coroutineQueue.Enqueue(LerpFunction());
To stop all running and queued coroutines you can now do:
coroutineQueue.Clear();
if (runningCoroutine != null)
StopCoroutine(runningCoroutine);
He said "When I call the Lerpfunction() while its still running, It jumps as soon as the first one is done. I don't want it to jump but rather start off again from where it stopped". So I'm sure he doesn't want to stack it. He asked an inappropriate question.
Thank you so much for your answer! I implemented your first suggestion and it works like a charm! :) Just out of curiosity, I needed a slight adjustment for it to work. To start/queue the coroutine I needed to change:
if (runningCoroutine == null)
{
runningCoroutine = StartCoroutine(LerpFunction());
}
else
{
coroutineQueue.Enqueue(LerpFunction());
}
To:
if (runningCoroutine == null)
{
runningCoroutine = LerpFunction();
StartCoroutine(runningCoroutine);
}
else
{
coroutineQueue.Enqueue(LerpFunction());
}
Otherwise I got the exception: "cannot implicitly convert type unityengine.coroutine to system.collections.ienumerator"
Do you know why I got this? Anyway thanks again! I learned allot from it!
Yes, my bad, that's what i wanted to write ^^. Great that you figured it out yourself. I'll fix my answer.
Answer by tormentoarmagedoom · Jan 16, 2019 at 07:43 PM
Good day.
First, i expect you know about
yield return new WaitforSeconds(10);
To "hold" the execution of that method for X seconds.
then, You only need to create a bool variable to know if its running,
IEnumerator Lerpfunction ()
{
yourbool = true;
//Do things
yield return new WaitForSeconds (10);
counter++;
yourbool=false;
}
So now, you can check for yourbool variable to know if it is running. If not, Run it again. You can also add a counter to know how many times you executed the function.
Bye!!
i think he wants to have 1 corroutine execute multiple times at the same moment, but thats not posible right?
Yes it is possible. It could cause complications if they share state, of course.
Thank you for your reply! I tried WaitForSeconds but I could not get it to work. @xxmariofer is right, I want to “save up my coroutine-calls” and execute them as soon as the first one is done. To give some more context; I am trying to make a bar that fills up. Right now, if I call the coroutine Lerpfunction() while it is still running this happens: (see GIF)
When I call the Lerpfunction() while its still running, It jumps as soon as the first one is done. I don't want it to jump but rather start off again from where it stopped. Lerpfunction:
IEnumerator Lerpfunction(float fromLerp, float toLerp, float lerpTime)
{
float startTime = Time.time;
while(Time.time < (startTime + lerpTime))
{
float t = (Time.time - startTime) / lerpTime;
myImage.fillAmount = $$anonymous$$athf.Lerp(fromLerp, toLerp, t);
yield return null;
}
myImage.fillAmount = toLerp;
}
Any Ideas?
Answer by haruna9x · Jan 17, 2019 at 02:36 AM
Not sure I understand what you are saying. I think you're doing a health bar. If yes, try it:
private IEnumerator enumerator;
// I don't use toLerp and formLerp, but use directly fillAmount.
// This will keep the fillAmount value softened continuously.
private IEnumerator Lerp(float finalValue, fload lerpDuration)
{
float lerpSpeed = Mathf.Abs(myImage.fillAmount - finalValue) / lerpDuration;
while (!Mathf.Approximately(myImage.fillAmount, finalValue))
{
myImage.fillAmount = Mathf.MoveTowards(myImage.fillAmount, finalValue, fadeSpeed * Time.deltaTime);
yield return null;
}
enumerator =null;
}
Use this to call:
if(enumerator !=null)
StopCoroutine(enumerator );
enumerator = Lerp(finalAlpha,lerpDuration);
StartCoroutine(enumerator);
Thanks for replying! That was not exactly what I was looking for but I can use you're answer on a different case haha! Thank you!
Answer by xxmariofer · Jan 16, 2019 at 07:36 PM
Cant you use aync methods? I think they can stacked but i can not tested it right now sorry.
https://docs.microsoft.com/es-es/dotnet/csharp/programming-guide/concepts/async/
Answer by aalami · 4 days ago
@Bunny83, Thank you for your great solution.
Just for future reference for anyone who is referring to this, I would like to suggest a small modification to your second solution.
Instead of having the CoroutineCoordinator running all the time, we can stop it when the queue is empty, then restart it whenever it is needed. This combines the best of both solutions you provided.
I created a simple class that you can have an instance of in your scripts:
public class QueueManager : MonoBehaviour
{
private readonly Queue<IEnumerator> _coroutineQueue = new Queue<IEnumerator>();
public void AddToQueue(IEnumerator coroutine)
{
_coroutineQueue.Enqueue(coroutine);
if (_coroutineQueue.Count ==1) //no previous items in queue
StartCoroutine(CoroutineCoordinator());
}
private IEnumerator CoroutineCoordinator()
{
while (true)
{
while (_coroutineQueue.Count > 0)
{
yield return StartCoroutine(_coroutineQueue.Peek());
_coroutineQueue.Dequeue();
}
if (_coroutineQueue.Count == 0)
yield break;
}
}
}
Then you can call it in your other class:
private QueueManager _animationQueue;
void Start()
{
_animationQueue= gameObject.AddComponent<QueueManager>();
}
// Then
_animationQueue.AddToQueue(LerpFunction())
I know that the change is very minor, but the continuous execution of the coroutine was bothering me, and the first solution didn't provide the decoupled code like the second one.
You won't gain much from that. Starting and stopping coroutines is actually quite expensive as new objects are created each time. Keeping it running is usually much cheaper. Also Unity now supports yielding IEnumerators directly and can automatically nest them without the need to call StartCoroutine. So using
yield return coroutineQueue.Dequeue();
instead of
yield return StartCoroutine(coroutineQueue.Dequeue());
would be cheaper.
Apart from that if you really want to stop the coroutine once the queue is empty you should restructure the code like this, even though I would not recommend it:
public class QueueManager : MonoBehaviour
{
private readonly Queue<IEnumerator> _coroutineQueue = new Queue<IEnumerator>();
private Coroutine coordinator = null;
public void AddToQueue(IEnumerator coroutine)
{
_coroutineQueue.Enqueue(coroutine);
if (coordinator == null)
coordinator = StartCoroutine(CoroutineCoordinator());
}
private IEnumerator CoroutineCoordinator()
{
while (_coroutineQueue.Count > 0)
yield return _coroutineQueue.Dequeue();
coordinator = null;
}
}
Your answer
Follow this Question
Related Questions
Multiple Cars not working 1 Answer
Distribute terrain in zones 3 Answers
IEnumerators saving passed value into two different variables. 1 Answer
How do I pause a script while until after a function is done running? 3 Answers
How to spawn evenly spaced game objects and move them in a circular path? 1 Answer