Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice

Would it make sense to have a CancelRoutine() ?

Discussion in 'Scripting' started by amigueloferreira, Jul 8, 2015.

  1. amigueloferreira

    amigueloferreira

    Joined:
    Jul 8, 2014
    Posts:
    4
    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.

    Code (CSharp):
    1. using System.Collections;
    2. using UnityEngine;
    3.  
    4. //Debug.Log ("End printing") will not be shown!
    5. public class EnumeratorProblem : MonoBehaviour {
    6.    
    7.     static int a = 0;
    8.     IEnumerator printRoutine = null;
    9.    
    10.     void Start() {
    11.         StartCoroutine (main ());
    12.         StartCoroutine (endPrinterAfter (4.0f));
    13.     }
    14.    
    15.     IEnumerator endPrinterAfter(float seconds) {
    16.         yield return new WaitForSeconds (seconds);
    17.         StopCoroutine (printRoutine);
    18.     }
    19.    
    20.     IEnumerator main() {
    21.         Debug.Log ("Begin printing");
    22.         printRoutine = printer ();
    23.         yield return StartCoroutine (printRoutine);
    24.         Debug.Log ("End printing");
    25.     }
    26.    
    27.     IEnumerator printer() {
    28.         while (true) {
    29.             Debug.Log("Printing "+(a++));
    30.             yield return new WaitForSeconds(0.5f);
    31.         }
    32.     }
    33. }
    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:

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public interface ICancellabeEnumerator : IEnumerator {
    5.     void Cancel();
    6.     bool IsCancelled();
    7. }
    8.  
    9. class CancellableEnumerator : ICancellabeEnumerator {
    10.    
    11.     private IEnumerator _wrappedEnumerator;
    12.     private bool _cancelled = false;
    13.    
    14.     public CancellableEnumerator(IEnumerator originalEnumerator) {
    15.         _wrappedEnumerator = originalEnumerator;
    16.     }
    17.    
    18.     public bool MoveNext()
    19.     {
    20.         return !_cancelled && _wrappedEnumerator.MoveNext ();
    21.     }
    22.    
    23.     public object Current
    24.     {
    25.         get
    26.         {
    27.             return _wrappedEnumerator.Current;
    28.         }
    29.     }
    30.    
    31.     public void Reset()
    32.     {
    33.         _wrappedEnumerator.Reset ();
    34.         _cancelled = false;
    35.     }
    36.    
    37.     public void Reset(IEnumerator enumerator) {
    38.         _cancelled = false;
    39.         _wrappedEnumerator = enumerator;
    40.     }
    41.    
    42.     public void Cancel() {
    43.         _cancelled = true;
    44.     }
    45.    
    46.     public bool IsCancelled() {
    47.         return _cancelled;
    48.     }
    49. }
    50.  
    51. public static class ICancellableEnumeratorExtensions {
    52.  
    53.     public static ICancellabeEnumerator Cancellable(this IEnumerator enumerator) {
    54.         return new CancellableEnumerator(enumerator);
    55.     }
    56.  
    57.     public static bool Cancel(this IEnumerator enumerator,bool ignoreNull = false) {
    58.  
    59.         if (ignoreNull && enumerator == null)
    60.             return false;
    61.  
    62.         ICancellabeEnumerator cancellableEnumerator = enumerator as ICancellabeEnumerator;
    63.  
    64.         if (cancellableEnumerator == null) {
    65.             Debug.LogError ("Trying to cancel a non-cancellable enumerator. Make sure you are using a cancellable enumerator by calling Cancellable()");
    66.             return false;
    67.         } else {
    68.             cancellableEnumerator.Cancel ();
    69.             return true;
    70.         }
    71.     }
    72. }
    73.  
    74. public static class MonoBehaviourCancelRoutineExtensions {
    75.  
    76.     public static bool CancelRoutine(this MonoBehaviour mono, IEnumerator enumerator, bool ignoreNull = false) {
    77.         return enumerator.Cancel ();
    78.     }
    79. }
    And you can use it like this:

    Code (CSharp):
    1. using System.Collections;
    2. using UnityEngine;
    3.  
    4. //Now Debug.Log ("End printing") will be shown when you cancel the inner coroutine
    5. public class CancellabeEnumeratorTest : MonoBehaviour {
    6.    
    7.     static int a = 0;
    8.     IEnumerator printRoutine = null;
    9.    
    10.     void Start() {
    11.         StartCoroutine (main ());
    12.         StartCoroutine (endPrinterAfter (4.0f));
    13.     }
    14.    
    15.     IEnumerator endPrinterAfter(float seconds) {
    16.         yield return new WaitForSeconds (seconds);
    17.         this.CancelRoutine (printRoutine);
    18.     }
    19.    
    20.     IEnumerator main() {
    21.         Debug.Log ("Begin printing");
    22.         printRoutine = printer ().Cancellable();
    23.         yield return StartCoroutine (printRoutine);
    24.         Debug.Log ("End printing");
    25.     }
    26.    
    27.     IEnumerator printer() {
    28.         while (true) {
    29.             Debug.Log("Printing "+(a++));
    30.             yield return new WaitForSeconds(0.5f);
    31.         }
    32.     }
    33. }

    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 https://gist.github.com/miguel12345/78189fb3c53128948c45
     

    Attached Files:

  2. blizzy

    blizzy

    Joined:
    Apr 27, 2014
    Posts:
    775
    In an ideal world, the outer coroutine would get some sort of exception from the call that started the nested coroutine. That doesn't seem to be the case for some reason, so the best course of action is to just pause it indefinitely.
     
  3. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
  4. amigueloferreira

    amigueloferreira

    Joined:
    Jul 8, 2014
    Posts:
    4
    Thank you, that does look cool, I'l take a look into it.