Search Unity

[FREE] More Effective Coroutines

Discussion in 'Assets and Asset Store' started by Trinary, Feb 23, 2016.

  1. EliasMasche

    EliasMasche

    Joined:
    Jul 11, 2014
    Posts:
    92
    Well bad for nested MEC Coroutines, but more improvements is great for me. remove all GC ALLOCS is going to be interesting.
     
  2. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Sorry, I guess I forgot to say in my previous post: Yes, in v1.4 you can yield one coroutine to another using "yield return Timing.HoldUntilDone(waitHandle);"

    I think that's what you mean by nested coroutines. Did you mean something else?
     
    idurvesh likes this.
  3. EliasMasche

    EliasMasche

    Joined:
    Jul 11, 2014
    Posts:
    92
    Wee, Great, yeah a coroutine inside a coroutine time to make corouception
     
    Trinary likes this.
  4. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Ok, version 1.4 is out, yay!

    1.4 - Fixed a bug where the wrong process would be killed when an exception was thrown in some cases.
    - Added HoldUntilDone which will allow process chaining, and one process waiting for another process.
    - Changed "StartUpdateCoroutine" and it's two cousins to a single function named RunCoroutine. You can now pass the segment timing you want to use in as an optional second argument.
    - Updated documentation.

    However, I just noticed today that Unity coroutines start the first frame when you call StartCoroutine.. whereas MEC have been starting the first frame on the next update. This caused an exception in the project I am working on.. so I have to assume that it may have affected some other people.

    It isn't hard to correct, and I'll submit the corrected version today.. I just worry about the number of people that may have run into this issue without saying anything.
     
  5. Marceta

    Marceta

    Joined:
    Aug 5, 2013
    Posts:
    177
    This is awesome, I don't know how i missed this before. How do i yield return www request?
     
    Trinary likes this.
  6. PhoenixRising1

    PhoenixRising1

    Joined:
    Sep 12, 2015
    Posts:
    488
    I just solved it by adding it to the end of script execution order.
     
    Trinary likes this.
  7. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    @Marceta You're in luck, I just added that in this last update :) "yield return Timing.WaitUntilDone(wwwObj);"

    @ephemeral life +1, that would work for most cases. It wouldn't work as well if you started it during the Start or Awake functions, but not everyone does that.

    I have this little thing where I hate having a setup process or using custom editor scripts. So I'm working on the logic to either run it now or finish out the loop.. even though it's turning out to be a bit convoluted.

    Also, there is a wrinkle I noticed in v1.4 that I just found where it won't always accept the timing segment you pass in. 1.4.1 will fix it, which is waiting in the approval queue now. 1.4.2 will come out right after that to finish up the startup timing logic.. and then I promise to stop these rapid updates.
     
    Marceta and PhoenixRising1 like this.
  8. PhoenixRising1

    PhoenixRising1

    Joined:
    Sep 12, 2015
    Posts:
    488
    I don't mind updates of any kind :). I love your support.
     
  9. idurvesh

    idurvesh

    Joined:
    Jun 9, 2014
    Posts:
    495
    Thanks for update...I recently come in contact with UniRx,...Is it possible to use MEC with UniRx too?
     
  10. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    UniRx uses Unity's coroutines in some places and multi-threaded code in other places, and I wouldn't know where to start to upgrade it so that the UniRx core used MEC coroutines.

    However, if you just want to import them both into the same project then that will work fine. Both our plugins play nice with others.
     
  11. Marceta

    Marceta

    Joined:
    Aug 5, 2013
    Posts:
    177
    @Trinary
    I couldn't find any method WaitUntilDone did you mean HoldUntilDone? I guess that's the one :)
     
    Last edited: Apr 15, 2016
  12. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    @Marceta you're right, there is a discrepancy. If I look at the way all the other files are named it looks like WaitUntilDone is the way it should be in order to fit in with other functions like WaitForSeconds. I'll go through the docs and the code this evening and make sure that everything is consistent.

    Thanks for bringing it to my attention.
     
    PhoenixRising1 likes this.
  13. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Well they didn't approve v1.4.1 last week, so I'm going to go ahead and skip to v1.5.

    Some really fun new things in v1.5: You can define your own exception handler in case you don't like the "swallow and log to console." Also, I've added a new update segment: SlowUpdate
     
    idurvesh and hopeful like this.
  14. idurvesh

    idurvesh

    Joined:
    Jun 9, 2014
    Posts:
    495
    Excited to hear about slowUpdate
     
  15. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Yeah, I've been working with it for a day and I keep asking myself why it isn't part of Unity. It's very useful for keeping track of things and displaying text to the user.

    Here's the doc page on it, http://trinary.tech/slowupdate/ hopefully it will be available by Tuesday.
     
    EliasMasche, montyfi and GiantGrey like this.
  16. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    1.5 is out!

    - Fixed a bug where the timing segment wouldn't always be set to what you thought it was.
    - RunCoroutine now starts the coroutine immediately. Previously the coroutine was just put into the buffer to be run next frame.
    - If a process holds for another process in a different timing segment it will now not move the held timing into it's segment.
    - Added an overload to KillCoroutineOnInstance that also returns the timing segment that the coroutine was killed in.
    - Renamed HoldUntilDone to WaitUntilDone for consistency.
    - KillCoroutine will no longer create a new Timing instance if there isn't one already.
    - Added an OnError callback that will be called if any coroutines throw exceptions.
    - Various null checks and format improvements.
    - Added the SlowUpdate segment.

    I suggest that if anyone is running v1.4 that they upgrade, due to the bug with the timing segment that was in that version.
     
    PhoenixRising1 and hopeful like this.
  17. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Ohh.. the namespace changed back to MovementEffects.. Sorry about all the changes to the API, but it turned out I needed to return it to MovementEffects so it could integrate cleanly with Movement/Time. MEC was weird anyway, as @S_Darkwell pointed out.
     
    S_Darkwell likes this.
  18. giraffe1

    giraffe1

    Joined:
    Nov 1, 2014
    Posts:
    302
    Hi,

    Thank you very much for sharing your work for free. I am going to give this a try.

    Can MEC return values?
     
    Trinary likes this.
  19. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Unfortunately I can't make any improvements over Unity's coroutines concerning returning values. In both implementations the best way to return a value is to pass in a shared class and read the value(s) of that class once the coroutine is finished.
     
  20. idurvesh

    idurvesh

    Joined:
    Jun 9, 2014
    Posts:
    495
    here are few things I noticed ,

    If you want to kill MEC you need to store instance of coroutine as global variable (so that you can use Timing.KillCoroutine(coroutineVariable) in OnDisable()


    If you want to kill MEC which is already waiting for another coroutine using WaitUntilDone, then you first need to kill that another coroutine before calling kill on this coroutine else coroutines will not get kill.
     
  21. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Thanks. That second is a bug. I'll fix it asap.

    I don't know what I can do with KillCoroutine, but I'm going to take a deeper look at it when I get a moment and see if I can make it work more like unity's does.
     
    idurvesh and hopeful like this.
  22. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    1,261
    I am experiencing a problem killing a coroutine.

    The code is setup somewhat like this.
    Code (CSharp):
    1. IEnumerator<float> DoStuff()
    2. {
    3.     while(true)
    4.     {
    5.         //Do stuff
    6.         float waitTime = Random.Range(0.3f, 0.7f);
    7.         yield return Timing.WaitForSeconds(waitTime);
    8.     }
    9. }
    When I run it I store the return value of RunCoroutine() so I can use it to kill the coroutine later.

    The problem is it doesn't stop. I have to change the while(true) to use a variable and set it to false when I call KillCoroutine() and then it will stop.
     
    Trinary likes this.
  23. idurvesh

    idurvesh

    Joined:
    Jun 9, 2014
    Posts:
    495
    can you post init code?
     
    Trinary likes this.
  24. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Using a shared variable and simply ending the function is certainly one way to stop a coroutine. You could also check the shared variable and call yield break when it's true, like this:
    Code (CSharp):
    1. if(IshouldStopRunning)
    2.     yield break;
    However, KillCoroutine should also work. It has been working for other people, so it must be something about the way you are trying to use it. If you can post that part of your code I think we should be able to figure this out.
     
    idurvesh likes this.
  25. SugoiDev

    SugoiDev

    Joined:
    Mar 27, 2013
    Posts:
    395
    Hello!

    First of all, thanks for proving MEC for the community!

    I'm trying to integrate MEC into my async event system and I've reached a pain point.
    The event system needs to be very fast and must allocate nothing, since it is the base of communication among all of my game's elements and runs many times per frame. Because of that, I cache IEnumerables that I use repeatedly to avoid creating a delegate for each invocation.


    Like so:

    Code (CSharp):
    1. int totalRuns;
    2. IEnumerator<float> SomeIEnumeratorCache;
    3.  
    4. IEnumerator<float> SomeIEnumerator() {
    5.         totalRuns++;
    6.         while (true) {
    7.             if (++count > 10) {
    8.                 yield break;
    9.             }
    10.             yield return 0f;
    11. }
    12.  
    13. void Start() {
    14.         SomeIEnumeratorCache = SomeIEnumerator;
    15. }
    Now, If I were using normal Unity's coroutines, I could say:

    Code (CSharp):
    1. StartCoroutine(SomeIEnumeratorCache);
    2. StartCoroutine(SomeIEnumeratorCache);
    And I would have totalRuns=2

    But with MEC, if I do

    Code (CSharp):
    1. Timing.RunCoroutine(SomeIEnumeratorCache);
    2. Timing.RunCoroutine(SomeIEnumeratorCache);
    I will have totalRuns=1.

    This only happens if I'm caching the IEnumerator.

    If I do

    Code (CSharp):
    1. Timing.RunCoroutine(SomeIEnumerator());
    2. Timing.RunCoroutine(SomeIEnumerator());
    It works as expected, with totalRuns=2, but it will allocate to pass the method.


    Is there a way to make MEC work with my cached IEnumerators?


    Thanks again and best regards!
     
  26. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    @SugoiDev That really is quite interesting. When you add the same instance of an IEnumerator to the queue twice in MEC that will make it execute two yield returns per frame. The only way that I know to ensure that multiple starts will end up running different instances of the function is to make more than one copy of that function. If that is what is happening then it would result in a GC alloc as your IEnumerator is being copied.

    MEC doesn't allocate any heap memory when starting, but creating the IEnumerator pointer does (which is often done in the same line of code as RunCoroutine). If what you say is true, then it may be that Unity copies extra data onto the heap while using StartCoroutine.

    Once you call yield break or end the coroutine function .net will close the Enumerator function out so that it cannot be executed any more. If you were to call RunCoroutine at that point it would immediately exit. In order to get around this, I have a system that I use internally which is not in the documentation: If you "yield return float.NaN;" then the coroutine will be taken out of the running queue but will not be closed. You can then use "Timing.RunCoroutine(yourCachedHandle);" to resume it where you left off.

    I don't want to copy IEnumerators because the main point of MEC is to keep allocations to a minimum, so you'll have to do it yourself, with something like this:
    Code (CSharp):
    1. List<IEnumerator<float>> contexts = new List<IEnumerator<flaot>>();
    2.  
    3. contexts.Add(_CoroutineFunc());
    4. contexts.Add(_CoroutineFunc());
    5. contexts.Add(_CoroutineFunc());
    6.  
    7. int i = 0;
    8. Timing.RunCoroutine(contexts[i++]);
    You could probably pass in a shared boolean for isRunning.
     
  27. after8sg

    after8sg

    Joined:
    Apr 1, 2016
    Posts:
    2
    Good day,

    1) How should I replace startcoroutine with named methodName ?
    2) Does WaitUntilDone() support static Coroutine function? I have warning on the console (HoldUntilDone cannot hold: The coroutine instance that was passed in was not found.)
     
    Last edited: Apr 29, 2016
  28. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Welcome to the forums after8.

    1) I'm not clear about what you are asking.
    2) static isn't a problem. The problem is that you need to pass the same pointer that was returned by RunCoroutine.
     
  29. Manny Calavera

    Manny Calavera

    Joined:
    Oct 19, 2011
    Posts:
    205
    Can this also be used as an alternative to InvokeRepeating()? I mean, the same functionality of InvokeRepeating() minus the reflection and perhaps the ability to stop it if necessary.
     
  30. after8sg

    after8sg

    Joined:
    Apr 1, 2016
    Posts:
    2
    Example : StartCoroutine("TestFunction") ==> Timeing.RunCoroutine(??)
     
  31. PhoenixRising1

    PhoenixRising1

    Joined:
    Sep 12, 2015
    Posts:
    488
    Timing.RunCoroutine(TestFunction());
    You can call unity's coroutines without using a string too, took me a while to find that out :).
     
    Trinary likes this.
  32. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Yes. In MEC I broke it into two different functions called CallDelayed and CallRepeatedly. I also use actions rather than the magic strings that unity seems to prefer.

    @after8sg I'm sorry, MEC doesn't use magic string based function calls. That style is an order of magnitude slower than function pointers, and if you spell anything incorrectly then you get a runtime error rather than a compile time error. If you want to use MEC then you are probably worried about performance, in which case I suggest you find alternatives to magic strings.
     
  33. Manny Calavera

    Manny Calavera

    Joined:
    Oct 19, 2011
    Posts:
    205
    Actually, CallRepeatedly is nowhere in the source code. Maybe it got removed accidentally at some point? According to the Asset Store window I have the latest version.
     
  34. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Sorry, it's CallContinously. I hope you'll give me a break, I have a day job and have to respond to this forum from my phone while riding the train.

    It's all in the documentation.
    http://trinary.tech/category/mec/
     
  35. Manny Calavera

    Manny Calavera

    Joined:
    Oct 19, 2011
    Posts:
    205
    Per documentation, CallContinously calls the specified action every frame over the next x seconds while InvokeRepeating calls the specified action every x seconds, not every frame.

    The implementation would be similar to CallContinously (yield for x seconds instead of 0f) so I was just pointing it out in case you want to complete your library with another useful function.

    Please respond at your convenience. No need to jump out of the train or anything. :D
     
  36. Gekigengar

    Gekigengar

    Joined:
    Jan 20, 2013
    Posts:
    738
    Anyone has done a performance stress test compared to the original coroutine?
     
  37. PhoenixRising1

    PhoenixRising1

    Joined:
    Sep 12, 2015
    Posts:
    488
    Can't you just use WaitForSeconds in a while loop?
     
  38. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    @Manny Calavera I'll look into that. Maybe I will call it call repeatedly? The list of changes and improvements is getting long enough that it would be worthwhile to make a new version soon.. It will be a couple of weeks before I have time to properly focus on it though.

    For now it might work to change your SlowUpdate rate to what you want and CallContinously in SlowUpdate.

    @Gekigengar One person got mad about my claims and tried to prove that Unity's coroutines were better. He got some weird results that I don't trust. He claimed that having MEC in the project made Unity's coroutines run slower, which makes no sense since I'm not hooking into Unity's coroutines in any way.

    I'll do comparisons when I get time, but I would love it if someone who was independent could test it. There seems to be several factors involved. The first coroutine to be run ever has to set up a few things, and Unity seems to do this too. Then there is coroutine startup speed/memory and coroutine running speed/memory. I think Unity is attaching it's buffers to the gameObjects in your scene, and I want to investigate whether Unity's coroutines actually make your gameObjects larger. So many things to test.
     
    hopeful and Gekigengar like this.
  39. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    1,261
    I am starting it in the normal way.

    Here is how I am using it.

    Code (CSharp):
    1. IEnumerator<float> coroutineCache;
    2.  
    3. public void StartFunction()
    4. {
    5.     coroutineCache = Timing.RunCoroutine(CoroutineToRun());
    6. }
    7.  
    8. IEnumerator<float> CoroutineToRun()
    9. {
    10.     while (true)
    11.     {
    12.         //Do Coroutine stuff
    13.         yield return Timing.WaitForSeconds(Random.Range(0.3f, 0.7f);
    14.     }
    15. }
    16.  
    17. public void StopRunningCoroutine()
    18. {
    19.     Timing.KillCoroutine(coroutineCache);
    20. }
    This should kill the coroutine but something is causing it to start back up again. Its probably somewhere else in my code that is calling the StartFunction(). It was working before I started to make some changes, but I upgraded MEC at the same time so I thought it might be a bug in the new version. Once I get some time I will investigate.
     
  40. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    If you have called StartCoroutine more than once then KillCoroutine will only kill the first instance that it finds. I really should change that too.

    Try this workaround, if it works we'll know that that is the problem:
    Code (CSharp):
    1. while(Timing.KillCoroutine(coroutineCache)) {}
     
  41. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    1,261
    Fixed it. It was a function being called on touch, it happened that It could be called over and over again while it was being touched, fixed it so it only calls it once. Still need to do more testing to ensure its working correctly, but so far so good.

    I have to thank you for creating this system. In my attempt to go virtually allocation free, this has helped greatly.
     
    Trinary likes this.
  42. fairchild670

    fairchild670

    Joined:
    Dec 3, 2012
    Posts:
    69
    Hi there! First off thanks for creating this awesome asset, not to mention giving it away for free. It's been quite helpful for us to reduce our GC and the implementation hasn't been too difficult to convert over. I have however run into an issue with the latest version, specifically the WaitUntilDone() function. Here's a quick example of the issue (note our project is in JS):

    Code (JavaScript):
    1. import System.Collections.Generic;
    2. import MovementEffects;
    3.  
    4. function Start() {
    5.     Timing.RunCoroutine(TestCoroutine(), Segment.Update);
    6. }
    7.  
    8. function TestCoroutine() : IEnumerator.<float> {
    9.     Debug.Log("TestCoroutine: Starting...");
    10.  
    11.     var handle : IEnumerator.<float> = Timing.RunCoroutine(TestWait(), Segment.Update);
    12.  
    13.     yield Timing.WaitUntilDone(handle);
    14.  
    15.     Debug.Log("TestCoroutine: Finished!");
    16. }
    17.  
    18. function TestWait() : IEnumerator.<float> {
    19.     for (var i : int = 0; i < 10; i++) {
    20.         Debug.Log("TestCoroutine: " + i);
    21.         yield;
    22.     }
    23. }
    I get the exception below after the first yield in TestWait() completes. (Side note: MainMenu.js is the file I ran the test in):

    The console output is:

    I was curious if I might have missed something or if this is a legit bug? Any help would be greatly appreciated!
     
    Trinary likes this.
  43. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Hmm.. it looks as though UnityScript may not be able to treat the value "NaN" as a float, and I am using NaN as a special flag in a couple of places in my code, including when one coroutine waits for another.

    Please check to see if this is actually the case. In the Timing class do a find and replace to replace all instances of "float.NaN" with "-1f" and then also change all calls to "float.IsNaN(<something>)" to "<something> == -1f".
     
  44. fairchild670

    fairchild670

    Joined:
    Dec 3, 2012
    Posts:
    69
    Thanks for the quick reply! I replaced the float.NaN vals and checks but it looks like I still get the same exact error. Just for comparison, the C# version of the same test code above still works fine.
     
  45. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Ok, I'm actually relieved that that wasn't it! I don't generally work in UnityScript, so I have to ask: In line 21 of what you posted above, you yield without yielding a value. It looks like there should be a value there. Could that be the issue?
     
  46. fairchild670

    fairchild670

    Joined:
    Dec 3, 2012
    Posts:
    69
    Tried yielding 0 there and still throws the same error. However, I think I may have found a temporary workaround...

    Not entirely sure what's going on, but long story short I did a little more experimenting with two versions of similar test code as above in both C# and UnityScript (TimingTest.cs and TimingTest.js) and it looks like the type of the object returned from RunCoroutine() is not quite exact between the languages. Actually only the C# test even makes it into Timing.WaitUntilDone(). Looks like the UnityScript error happens during the parameter casting/conversion?

    Anyway, I ended up adding this function to Timing.cs with only a single parameter:

    Code (CSharp):
    1. public static float WaitUntilFinished(IEnumerator<float> otherCoroutine) {
    2.             return WaitUntilDone(otherCoroutine, true, null);
    3.         }
    and used that in the UnityScript version and sure enough it seems to work fine.
     
  47. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    You're right. Unity's js seems to have some problem with defining default values. The bool warnIfNotFound = true seems to be tripping it up. It looks like it works with no changes if you go ahead and define all three parameters for that function, but that's cumbersome. I'll change the function signature so that js is ok with it in the next version.

    Thank you for the js snippit. If you don't mind I'd like to include your testing function in the documentation, for anyone who needs a js example.
     
  48. fairchild670

    fairchild670

    Joined:
    Dec 3, 2012
    Posts:
    69
    Yeah, absolutely and thanks again for the help!
     
    Trinary likes this.
  49. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Ok, the new version should work better in js. StartCoroutine, WaitUntilDone, and a couple of the Run.. functions are defined a little differently. (but as far as C# is concerned they are exactly the same)
     
    fairchild670 likes this.
  50. fairchild670

    fairchild670

    Joined:
    Dec 3, 2012
    Posts:
    69
    Hi - Thanks for the JS updates! Will try them soon.

    I have another question, or maybe issue, related to multiple instances of Timing and the WaitUntilDone() function.

    Basically the issue I'm running into is if I run a coroutine on a private instance of Timing and call WaitUntilDone() even from that instance, it actually does the waiting on the global static instance created at runtime. This works fine if run from the same scene, but if scenes change (where the private Timing instance uses DontDestoryOnLoad), there are warnings from the global static instance of Timing after the next scene loads that coroutines cannot be found. Here's a very quick code example from memory:

    Code (JavaScript):
    1. private var playerTiming : Timing;
    2.  
    3. function Initialize() : IEnumerator.<float> {
    4.  
    5. playerTiming = transform.GetComponent(Timing);
    6.  
    7. // function could last 15 seconds or more...
    8. var handle : IEnumerator.<float> = playerTiming.RunCoroutineOnInstance(HandleBreathing(), Segment.Update);
    9.  
    10. // scene could change during this yield
    11. yield playerTiming.WaitUntilDone(handle);
    12. }
    Is there a way to use the existing WaitUntilDone() or is it possible to add a WaitUntilDoneOnInstance() function?