Hi,
When you are dealing with nested coroutines, stopping an inner routine, will actually pause it’s entire “cotourine stack”.
What if you actually wanted to cancel an inner routine, but have the “outer” coroutine (the one that is waiting for it to complete) to continue?
To sum this up, let’s see the problem first.
using System.Collections;
using UnityEngine;
//Debug.Log ("End printing") will not be shown!
public class EnumeratorProblem : MonoBehaviour {
static int a = 0;
IEnumerator printRoutine = null;
void Start() {
StartCoroutine (main ());
StartCoroutine (endPrinterAfter (4.0f));
}
IEnumerator endPrinterAfter(float seconds) {
yield return new WaitForSeconds (seconds);
StopCoroutine (printRoutine);
}
IEnumerator main() {
Debug.Log ("Begin printing");
printRoutine = printer ();
yield return StartCoroutine (printRoutine);
Debug.Log ("End printing");
}
IEnumerator printer() {
while (true) {
Debug.Log("Printing "+(a++));
yield return new WaitForSeconds(0.5f);
}
}
}
If you run this behaviour, you will see that the “End printing” log will never be called, because stopping the printRoutine will actually pause all it’s parent routines.
I tried a “cancellable” approach, by wrapping the IEnumerator in a class:
using UnityEngine;
using System.Collections;
public interface ICancellabeEnumerator : IEnumerator {
void Cancel();
bool IsCancelled();
}
class CancellableEnumerator : ICancellabeEnumerator {
private IEnumerator _wrappedEnumerator;
private bool _cancelled = false;
public CancellableEnumerator(IEnumerator originalEnumerator) {
_wrappedEnumerator = originalEnumerator;
}
public bool MoveNext()
{
return !_cancelled && _wrappedEnumerator.MoveNext ();
}
public object Current
{
get
{
return _wrappedEnumerator.Current;
}
}
public void Reset()
{
_wrappedEnumerator.Reset ();
_cancelled = false;
}
public void Reset(IEnumerator enumerator) {
_cancelled = false;
_wrappedEnumerator = enumerator;
}
public void Cancel() {
_cancelled = true;
}
public bool IsCancelled() {
return _cancelled;
}
}
public static class ICancellableEnumeratorExtensions {
public static ICancellabeEnumerator Cancellable(this IEnumerator enumerator) {
return new CancellableEnumerator(enumerator);
}
public static bool Cancel(this IEnumerator enumerator,bool ignoreNull = false) {
if (ignoreNull && enumerator == null)
return false;
ICancellabeEnumerator cancellableEnumerator = enumerator as ICancellabeEnumerator;
if (cancellableEnumerator == null) {
Debug.LogError ("Trying to cancel a non-cancellable enumerator. Make sure you are using a cancellable enumerator by calling Cancellable()");
return false;
} else {
cancellableEnumerator.Cancel ();
return true;
}
}
}
public static class MonoBehaviourCancelRoutineExtensions {
public static bool CancelRoutine(this MonoBehaviour mono, IEnumerator enumerator, bool ignoreNull = false) {
return enumerator.Cancel ();
}
}
And you can use it like this:
using System.Collections;
using UnityEngine;
//Now Debug.Log ("End printing") will be shown when you cancel the inner coroutine
public class CancellabeEnumeratorTest : MonoBehaviour {
static int a = 0;
IEnumerator printRoutine = null;
void Start() {
StartCoroutine (main ());
StartCoroutine (endPrinterAfter (4.0f));
}
IEnumerator endPrinterAfter(float seconds) {
yield return new WaitForSeconds (seconds);
this.CancelRoutine (printRoutine);
}
IEnumerator main() {
Debug.Log ("Begin printing");
printRoutine = printer ().Cancellable();
yield return StartCoroutine (printRoutine);
Debug.Log ("End printing");
}
IEnumerator printer() {
while (true) {
Debug.Log("Printing "+(a++));
yield return new WaitForSeconds(0.5f);
}
}
}
What do you guys think? Is this too contrived? This problem comes up frequently as I need to “stop” an inner routine, but I must make sure that it’s parent routine keeps running.
If you want to see this in a gist, check MonoBehaviour's CancelRoutine() that doesn't just pause a routine like StopRoutine() does · GitHub
2194265–145600–EnumeratorProblem.cs (713 Bytes)
2194265–145601–CancellabeEnumerator.cs (1.7 KB)
2194265–145602–CancellabeEnumeratorTest.cs (774 Bytes)