Search Unity

Extended coroutines

Discussion in 'Scripting' started by ArkaneX, Sep 23, 2013.

  1. ArkaneX

    ArkaneX

    Joined:
    Jul 21, 2012
    Posts:
    14
    Hello all,

    Today, at Unity Answers, Fattie posted a question about detecting if coroutine is still running.

    I don't know of any method of detecting this, and I don't think any simple method exists, apart from setting some variables at coroutine end. Anyway, this triggered my curiosity, so I entered research mode. As a result of my quick R&D, I created StartCoroutineEx extension method, CoroutineController class and CoroutineState enumeration. Please see the code below, but firstly some info.

    1. To use extended version, instead of calling:
    Code (csharp):
    1. StartCoroutine(SomeCoroutine());
    you have to use:
    Code (csharp):
    1. CoroutineController controller;
    2. this.StartCoroutineEx(SomeCoroutine(), out controller); // please note that 'this' keyword is required (at least in C#)
    2. Using CoroutineController returned as output parameter, you can check current state of coroutine, as well as control it a bit (stop, pause, resume).

    3. You can yield it as a standard coroutine as well:
    Code (csharp):
    1. yield return this.StartCoroutineEx(SomeCoroutine(), out controller);
    A word of explanation is required here. When I started, I planned StartCoroutineEx to return instance of CoroutineEx, where CoroutineEx would be a class derived from YieldInstruction (I was not able to derive from Coroutine, as it is sealed). Everything was working great, except of yielding. After a few tries, I decided it's a dead end, as this is probably controlled within Unity engine, so returning anything but Coroutine will not be delayed. I went with out parameter then, but if anyone has a different suggestion, please share it.

    4. I used C#, because it's my almost native language ;) But if you put my scripts in any of early compilation folder (e.g. Assets\Plugins), then you can use them in UnityScript as well.

    5. This is an alpha version, so if you see an error, let me know. Please let me know as well, what do you think of this idea. Is it good? Or maybe stupid? Or maybe not really stupid, but you would never use it?

    Code:

    Code (csharp):
    1. public enum CoroutineState
    2. {
    3.     Ready,
    4.     Running,
    5.     Paused,
    6.     Finished
    7. }
    Code (csharp):
    1. using System.Collections;
    2.  
    3. public class CoroutineController
    4. {
    5.     private IEnumerator _routine;
    6.  
    7.     public CoroutineState state;
    8.  
    9.     public CoroutineController(IEnumerator routine)
    10.     {
    11.         _routine = routine;
    12.         state = CoroutineState.Ready;
    13.     }
    14.  
    15.     public IEnumerator Start()
    16.     {
    17.         if (state != CoroutineState.Ready)
    18.         {
    19.             throw new System.InvalidOperationException("Unable to start coroutine in state: " + state);
    20.         }
    21.  
    22.         state = CoroutineState.Running;
    23.         while (_routine.MoveNext())
    24.         {
    25.             yield return _routine.Current;
    26.             while (state == CoroutineState.Paused)
    27.             {
    28.                 yield return null;
    29.             }
    30.             if (state == CoroutineState.Finished)
    31.             {
    32.                 yield break;
    33.             }
    34.         }
    35.  
    36.         state = CoroutineState.Finished;
    37.     }
    38.  
    39.     public void Stop()
    40.     {
    41.         if (state != CoroutineState.Running  state != CoroutineState.Paused)
    42.         {
    43.             throw new System.InvalidOperationException("Unable to stop coroutine in state: " + state);
    44.         }
    45.  
    46.         state = CoroutineState.Finished;
    47.     }
    48.  
    49.     public void Pause()
    50.     {
    51.         if (state != CoroutineState.Running)
    52.         {
    53.             throw new System.InvalidOperationException("Unable to pause coroutine in state: " + state);
    54.         }
    55.  
    56.         state = CoroutineState.Paused;
    57.     }
    58.  
    59.     public void Resume()
    60.     {
    61.         if (state != CoroutineState.Paused)
    62.         {
    63.             throw new System.InvalidOperationException("Unable to resume coroutine in state: " + state);
    64.         }
    65.  
    66.         state = CoroutineState.Running;
    67.     }
    68. }
    Code (csharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public static class CoroutineExtensions
    5. {
    6.     public static Coroutine StartCoroutineEx(this MonoBehaviour monoBehaviour, IEnumerator routine, out CoroutineController coroutineController)
    7.     {
    8.         if (routine == null)
    9.         {
    10.             throw new System.ArgumentNullException("routine");
    11.         }
    12.  
    13.         coroutineController = new CoroutineController(routine);
    14.         return monoBehaviour.StartCoroutine(coroutineController.Start());
    15.     }
    16. }
     
    Last edited: Sep 24, 2013
    ted537 and Kirienko like this.
  2. Fattie

    Fattie

    Joined:
    Jul 5, 2012
    Posts:
    476
    Looks amazing, will investigate.
     
  3. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Could you wrap a normal Coroutine in a static class which keeps a list of currently running Coroutines, and use that list to see what's currently running?
     
  4. exiguous

    exiguous

    Joined:
    Nov 21, 2010
    Posts:
    1,749
  5. ArkaneX

    ArkaneX

    Joined:
    Jul 21, 2012
    Posts:
    14
    If you mean standard Coroutine (as an object returned from StartCoroutine), then I don't think you can do this, as there is no way to tell when it finished. You can of course keep reference to original IEnumerator passed to StartCoroutine, and check its Current property, but I think it's not a good approach.

    Thank you for this link! I haven't seen it before, but in fact I haven't been searching for anything... :oops:

    Anyway - my approach is a bit different, as it allows you to work with coroutines in a way similar to using standard coroutines (e.g. yielding). But I like the code from linked thread (especially finished notification event). I'll update my code if there will be a demand for this ;)
     
  6. dbrizov

    dbrizov

    Joined:
    Feb 22, 2014
    Posts:
    10
    Great mate, just what I was looking for. Works perfectly. I needed to start a couple of coroutines with different durations all at once, and when all are finished to call an event. Thank you very much.
     
    Last edited: Jul 29, 2014
  7. arklay_corp

    arklay_corp

    Joined:
    Apr 23, 2014
    Posts:
    242
    if you dont mind the code overhead (or if you already use it), I would recomend to include the parse plugin.

    It implements System.Threading.Tasks which is VERY useful for concurrency
     
  8. Sharp-Development

    Sharp-Development

    Joined:
    Nov 14, 2013
    Posts:
    353
    Uhm, as far as I know the System.Threading.Tasks namespace is only available in net 4.0 and higher. (In case you dont know, unity uses a mono version which is a counterpart to .net 3.5, give or take)
     
  9. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    You are aware they don't run in parallel, right? They are still all monothreaded, and ran one after the other.
     
    dbrizov likes this.
  10. dbrizov

    dbrizov

    Joined:
    Feb 22, 2014
    Posts:
    10
    Yes I know, but it somehow simulates multithreading . I mean, the coroutines I start all together, are not with equal duration, and I don't know which one is actually the longest, but I need to trigger the event when all are finished. This code really helped me. I am gonna edit the post, so I don't confuse the people. Thanks for the note.
     
    Last edited: Jul 29, 2014
  11. dbrizov

    dbrizov

    Joined:
    Feb 22, 2014
    Posts:
    10
    Where I can get this plugin from? And isn't Unity's API not thread safe?
     
  12. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    "not safe" is an understatement. You cannot access or modify any of Unity's object or method from any thread except the main one. Exception to this are structs that are fully on the managed side, like Vectors, Quaternion, Color...
     
    dbrizov likes this.
  13. ArkaneX

    ArkaneX

    Joined:
    Jul 21, 2012
    Posts:
    14
    I'm glad you found the code useful :)
     
  14. A.Killingbeck

    A.Killingbeck

    Joined:
    Feb 21, 2014
    Posts:
    483
    Wouldn't yielding in some other coroutine not accomplish the same thing?
    Code (CSharp):
    1. IEnumerator TheYielder(IEnumerator me)
    2. {
    3.   while(me.moveNext())
    4.   {
    5.     //me Still active
    6.      yield return null;
    7.   }
    8. }
     
  15. ArkaneX

    ArkaneX

    Joined:
    Jul 21, 2012
    Posts:
    14
    Andrew - not exactly. Your code would work, if you yielded original enumerator value instead of null (unless MoveNext returns false). But the whole point of my code was to introduce a wrapper, which allows you to easily control coroutines. So apart from checking if it is still running, you can pause, resume, etc.
     
  16. dbrizov

    dbrizov

    Joined:
    Feb 22, 2014
    Posts:
    10
    So I can do some background calculations, but if I want to change the scale of an object I won't be able to do it?
     
  17. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Exact!
     
    dbrizov likes this.
  18. Sharp-Development

    Sharp-Development

    Joined:
    Nov 14, 2013
    Posts:
    353
    No, they aswell dont "somehow simultate multithreading". I know what you want to say but its simply like comparing apples with steaks. Coroutines are method's which are paused and resumed lateron. Its nothing even similar to multithreading.
    When multithreading a method, it might aswell be paused and resumed, but you do that by pausing the whole thread without having anything inbetween. Thats still not the point of multithreading, but instead moving a method from your operating mainthread to another thread, most likely on another core, so not consuming any of your mainthreads time. This is real parallism (as long as the method runs on another core of course). Whereas coroutines always run on the same thread, just because of the fact of them being able to be paused, doesnt make them anywhere similar to multithreading.

    My opinion. -
     
  19. arklay_corp

    arklay_corp

    Joined:
    Apr 23, 2014
    Posts:
    242
    I know, but Parse's plugin implements it and works great
    (I used to use it a lot in .NET development and I was missing it in unity)
     
  20. arklay_corp

    arklay_corp

    Joined:
    Apr 23, 2014
    Posts:
    242
    Unity asset store, search "Parse" and download their official plugin (you dont even need a parse account)

    As they told you the limitation is to call unity's objects from the main thread, but switching to the main thread is very easy (if using Tasks), I can post some example code if you are interested
     
  21. dbrizov

    dbrizov

    Joined:
    Feb 22, 2014
    Posts:
    10
    T
    Thanks, I will give it a try :)
     
  22. Kirienko

    Kirienko

    Joined:
    Apr 5, 2013
    Posts:
    37
    Thank you very much for this gem.

    I have change the extension a bit to make it return the CoroutineController instead of the Unity's Coroutine object that I don't use for anything.

    This is what I have changed:

    Code (CSharp):
    1. public static class CoroutineExtensions
    2. {
    3.     public static CoroutineController StartCoroutineEx(this MonoBehaviour monoBehaviour, IEnumerator routine)
    4.     {
    5.         if (routine == null)
    6.         {
    7.             throw new System.ArgumentNullException("routine");
    8.         }
    9.      
    10.         CoroutineController coroutineController = new CoroutineController(routine);
    11.         monoBehaviour.StartCoroutine(coroutineController.Start());
    12.         return coroutineController;
    13.     }
    14. }
     
  23. ArkaneX

    ArkaneX

    Joined:
    Jul 21, 2012
    Posts:
    14
    Hi Kirienko - glad you like it :)

    I agree that returning a controller is a better idea, and only the issue mentioned in p. 3 prevented me from doing it (I wanted to stay as close to the original as possible). But if you'll never yield a result, then your modification is better.
     
  24. Kirienko

    Kirienko

    Joined:
    Apr 5, 2013
    Posts:
    37
    Hi ArkaneX,

    This is what I have figured out for the yielding: I have a coroutine member to refer the Coroutine object for yielding so you can:

    Code (CSharp):
    1. yield return this.StartCoroutineEx(SomeCoroutine()).coroutine;
    No doubt your approach is closer to the original way.
    I have also added an onFinish event inspired in the other coroutines extension code referenced in this thread.

    This is my modified code (at the moment):

    Code (CSharp):
    1. public class CoroutineController
    2. {
    3.     public delegate void OnFinish(CoroutineController coroutineController);
    4.  
    5.     public event OnFinish onFinish;
    6.  
    7.     private IEnumerator _routine;
    8.     private Coroutine _coroutine;  
    9.     private CoroutineState _state;
    10.  
    11.     public CoroutineController(IEnumerator routine)
    12.     {
    13.         _routine = routine;
    14.         _state = CoroutineState.Ready;
    15.     }
    16.  
    17.     public void StartCoroutine(MonoBehaviour monoBehaviour)
    18.     {
    19.         _coroutine = monoBehaviour.StartCoroutine(Start());
    20.     }
    21.  
    22.     private IEnumerator Start()
    23.     {
    24.         if (_state != CoroutineState.Ready)
    25.         {
    26.             throw new System.InvalidOperationException("Unable to start coroutine in state: " + _state);
    27.         }
    28.      
    29.         _state = CoroutineState.Running;
    30.         while (_routine.MoveNext())
    31.         {
    32.             yield return _routine.Current;
    33.             while (_state == CoroutineState.Paused)
    34.             {
    35.                 yield return null;
    36.             }
    37.             if (_state == CoroutineState.Finished)
    38.             {
    39.                 yield break;
    40.             }
    41.         }
    42.      
    43.         _state = CoroutineState.Finished;
    44.  
    45.         if(onFinish != null)
    46.             onFinish(this);
    47.     }
    48.  
    49.     public void Stop()
    50.     {
    51.         if (_state != CoroutineState.Running  && _state != CoroutineState.Paused)
    52.         {
    53.             throw new System.InvalidOperationException("Unable to stop coroutine in state: " + _state);
    54.         }
    55.      
    56.         _state = CoroutineState.Finished;
    57.     }
    58.  
    59.     public void Pause()
    60.     {
    61.         if (_state != CoroutineState.Running)
    62.         {
    63.             throw new System.InvalidOperationException("Unable to pause coroutine in state: " + _state);
    64.         }
    65.      
    66.         _state = CoroutineState.Paused;
    67.     }
    68.  
    69.     public void Resume()
    70.     {
    71.         if (_state != CoroutineState.Paused)
    72.         {
    73.             throw new System.InvalidOperationException("Unable to resume coroutine in state: " + state);
    74.         }
    75.      
    76.         _state = CoroutineState.Running;
    77.     }
    78.  
    79.     public CoroutineState state
    80.     {
    81.         get { return _state;}
    82.     }
    83.  
    84.     public Coroutine coroutine
    85.     {
    86.         get { return _coroutine; }
    87.     }
    88.  
    89.     public IEnumerator routine
    90.     {
    91.         get { return _routine; }
    92.     }
    93.  
    94. }
    95.  
    96. public static class CoroutineExtensions
    97. {
    98.     public static CoroutineController StartCoroutineEx(this MonoBehaviour monoBehaviour, IEnumerator routine)
    99.     {
    100.         if (routine == null)
    101.         {
    102.             throw new System.ArgumentNullException("routine");
    103.         }
    104.      
    105.         CoroutineController coroutineController = new CoroutineController(routine);
    106.         coroutineController.StartCoroutine(monoBehaviour);
    107.         return coroutineController;
    108.     }
    109. }
    110.  
     
  25. ArkaneX

    ArkaneX

    Joined:
    Jul 21, 2012
    Posts:
    14
    I like your approach - having a Coroutine as a member is a good idea!
     
    Kirienko likes this.
  26. IAmACoder

    IAmACoder

    Joined:
    Sep 14, 2015
    Posts:
    1
    This one is exception safe. (Means if your coroutine encountered an exception it will still finish properly.) Also call onFinish in all circumstances, whether it was manual state change by coder, by exception, or what's not.

    Code (CSharp):
    1. private IEnumerator Start()
    2.     {
    3.         if (_state != CoroutineState.Ready)
    4.         {
    5.             throw new System.InvalidOperationException("Unable to start coroutine in state: " + _state);
    6.         }
    7.  
    8.          _state = CoroutineState.Running;
    9.          do
    10.          {
    11.              try
    12.              {
    13.                  if (!_routine.MoveNext())
    14.                  {
    15.                      _state = CoroutineState.Finished;
    16.                  }
    17.              }
    18.              catch (System.Exception ex)
    19.              {
    20.                  Debug.LogError("Exception in coroutine: " + ex.Message);
    21.                  _state = CoroutineState.Finished;
    22.                  break;
    23.              }
    24.  
    25.              yield return _routine.Current;
    26.              while (_state == CoroutineState.Paused)
    27.              {
    28.                  yield return null;
    29.              }
    30.          }
    31.          while (_state == CoroutineState.Running);
    32.  
    33.         _state = CoroutineState.Finished;
    34.  
    35.         if (onFinish != null)
    36.             onFinish(this);
    37.     }