I recently dove into the wonderful world of coroutines! I have a sequence of methods that are required to dynamically generate avatars, and after extreme optimization, I got it down to 200 ms ish. There is a lot of processing going on, including high res map generation, etc.
Anyhoo, the remaining bit could not be threaded since they use many Unity methods which do not support multithreading, so I took a crack at dividing them up into coroutines with lots of yields to smooth it out, and it’s working great now. Hardly noticeable on the framerate now when a character instantiates.
My question is this… is there a reasonably elegant way of forcing the coroutines to behave as if they were not? I think it would be much easier to debug that way. So if there was a difficult issue, I could flip the switch and the coroutine and nested coroutines all ran in the same frame.
If not, that’s ok, but it wold be a nice option. I tried abstracting them so I could achieve this manually myself, but ran into several “gotchas”.
Coroutines are just a creative use of enumerables, so… I haven’t tried this, but it seems like you should be able to force a coroutine to run instantly if you replace
IEnumerator enumerator= MyFunction();
while (enumerator.MoveNext()) {
// we don't need to do anything here...
Debug.Log("My coroutine yield returned a " + enumerator.Current);
}
Using MoveNext works, but foreach does not since the IEnumerator does not implement GetEnumerator(). So, that’s all great. With my current setup, it’s only the beginning I suppose. It does not consider the fact that I am yielding to other IEnumerator methods from within the main Coroutine like this…
public IEnumerator SetupCharacter()
{
...
yield return SomeOtherMethod();
...
}
but then the R word came to mind ( recursion ) and I was able to do it like this…
void RunCoroutineManually(IEnumerator enumerator)
{
if (enumerator == null)
return;
while (enumerator.MoveNext())
{
// we don't need to do anything here...
UnityEngine.Debug.Log("My coroutine yield returned a " + enumerator.Current);
RunCoroutineManually(enumerator.Current as IEnumerator);
}
}
It seems so simple and trivial, but I’m fairly confident that I would not have though of doing it. Thanks you guys! You made my day. Sending good vibes in your general directions
So, just in case this is useful to anyone else, here is my implementation. Turning off the coroutines via Mode.RunImmediately makes it easier to debug and profile the code so you can easily see how long the whole thing takes, before any coroutine magic.
Then, when writing the actual coroutines, I try to yield as liberally as possible. I’m experimenting with a dynamic yield that measures the time elapsed since the beginning of the Update(). Coroutines run right after Update… see the following link…
so, if time elapsed is greater than a threshold ( say 10 ms ), then yield. Otherwise keep going. Not 100% certain I will proceed with that, we’ll see.
Final note, if you notice the waitForCompletion parameter for RunCoroutine, it allows you to write a method as a coroutine, but use it as if it’s not on a case by case basis. This can prevent code duplication, so you don’t need two versions of the same code ( which is the devil incarnate ).
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using UnityEngine;
namespace MyNamespace
{
public class CoroutineManager : MonoBehaviour
{
public enum Mode
{
UseCoroutines,
RunImmediately
}
const string disableBaseCoroutineError = "Please use RunCoroutine() instead of StartCoroutine() method(s)";
const string singletonError = "Please create only one instance of CoroutineManager";
public static CoroutineManager instance { get; private set; }
public Mode mode = Mode.UseCoroutines;
void Awake()
{
if (instance != null)
Error(singletonError);
instance = this;
}
public void RunCoroutine(IEnumerator coroutine, bool waitForCompletion = false)
{
bool startAsCoroutine = (mode == Mode.UseCoroutines) && !waitForCompletion;
if (startAsCoroutine)
base.StartCoroutine(coroutine);
else
StartCoroutineManually(coroutine);
}
private void StartCoroutineManually(IEnumerator coroutine)
{
if (coroutine != null)
while (coroutine.MoveNext())
StartCoroutineManually(coroutine.Current as IEnumerator);
}
// Disable using the base StartCoroutines directly
public new Coroutine StartCoroutine(string methodName)
{
Error(disableBaseCoroutineError);
return null;
}
public new Coroutine StartCoroutine(IEnumerator routine)
{
Error(disableBaseCoroutineError);
return null;
}
public new Coroutine StartCoroutine(string methodName, [DefaultValue("null")] object value)
{
Error(disableBaseCoroutineError);
return null;
}
public new Coroutine StartCoroutine_Auto(IEnumerator routine)
{
Error(disableBaseCoroutineError);
return null;
}
private void Error(string msg)
{
// Error reporting mechanism of your choice goes here :smile:
throw new System.InvalidOperationException("Error: " + msg);
}
}
}